Neo4j图数据库管理系统开发笔记之三:构建安全的RMI Service(Server)

 

RMI Server(服务端)主要包括以下功能:远程用户权限验证管理、远程服务接口实现类、Neo4j实体映射转换等。项目目录结构如下图所示:

rmi_server_project

3.2.1 远程用户权限验证管理
3.2.1.1 用户权限验证机制

用户权限验证机制分为三个层级。

第一级,远程主机IP地址验证。检查是否允许远程主机IP地址访问RMI服务。

第二级,远程用户信息验证。检查用户名称和密码是否正确,用户是否启用等。

第三级,远程服务及接口方法验证。检查用户是否有权访问某个RMI服务以及服务下的指定接口方法。

3.2.1.2 远程用户配置信息

远程用户配置信息在文件remote.users.config.xml中,内容格式如下表所示:

<?xml version="1.0"?>
<remote-users>
    <remote-user user-id="1" login-name="admin" password="admin" user-name="管理员用户" enabled="true"></remote-user>
    <remote-user user-id="2" login-name="test" password="test" user-name="测试用户" enabled="true"></remote-user>
</remote-users>
3.2.1.3 远程主机配置信息

远程主机配置信息在文件remote.hosts.config.xml中,内容格式如下表所示:

<?xml version="1.0"?>
<hosts>
    <allow-hosts>
        <host>*</host>
    </allow-hosts>
    <forbid-hosts>
        <host></host>
    </forbid-hosts>
</hosts>
3.2.1.4 用户权限配置信息

用户权限配置信息在文件remote.users.permission.xml中,内容格式如下表所示:

<?xml version="1.0"?>
<remote-users>
    <remote-user login-name="admin,test">
        <remote-service name="neo4j-graph-manage-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-node-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-index-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-path-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-cypher-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
    </remote-user>
</remote-users>
3.2.2 远程服务接口实现类

绝大部分的非业务类工作都是在远程服务基础接口实现类BaseRemoteServiceImpl中完成了,譬如,获取图数据库服务对象实例、用户权限验证、日志记录、Neo4j实体映射转换等。如下表所示:

package com.hnepri.neo4j.rmi.service;

import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;

import com.hnepri.common.util.DateTimeUtil;
import com.hnepri.neo4j.client.rmi.bean.GDirection;
import com.hnepri.neo4j.client.rmi.bean.GNode;
import com.hnepri.neo4j.client.rmi.bean.GPage;
import com.hnepri.neo4j.client.rmi.bean.GPath;
import com.hnepri.neo4j.client.rmi.bean.GRelationship;
import com.hnepri.neo4j.client.rmi.service.IBaseRemoteService;
import com.hnepri.neo4j.client.rmi.util.RemoteClientFactory;
import com.hnepri.neo4j.client.rmi.util.RemoteLoginStatusUtil;
import com.hnepri.neo4j.common.model.GraphPageModel;
import com.hnepri.neo4j.common.util.GraphTemplate;
import com.hnepri.neo4j.rmi.bean.RemoteUser;
import com.hnepri.neo4j.rmi.util.RemoteHostUtil;
import com.hnepri.neo4j.rmi.util.RemoteServerFactory;
import com.hnepri.neo4j.rmi.util.RemoteUserPermissionUtil;
import com.hnepri.neo4j.rmi.util.RemoteUserUtil;

/**
 * Description: 远程服务基类实现类<br>
 * Copyright: Copyright (c) 2015<br>
 * Company: 河南电力科学研究院智能电网所<br>
 * @author shangbingbing 2015-09-01编写
 * @version 1.0
 */
@SuppressWarnings("deprecation")
public class BaseRemoteServiceImpl extends UnicastRemoteObject implements IBaseRemoteService {
    private static final long serialVersionUID = 7292764643219275924L;
    private String loginName = "";
    private String password = "";
    private String serviceName = "";
    private String clientAddress = "";
    private boolean loginStatus = false;
    private String loginStatusMessage = "";
    private String graphName;
    private String graphPath;
    
    /**
     * 获取图数据库操作实例GraphTemplate
     * @return
     */
    public GraphTemplate getTemplate() {
        if(StringUtils.isNotBlank(this.getGraphName())) {
            return GraphTemplate.getInstanceByName(this.getGraphName());    
        } else {
            return GraphTemplate.getInstance(this.getGraphPath());
        }
    }
    
    public BaseRemoteServiceImpl() throws RemoteException {
        super();
    }
    
    /**
     * 获取登录用户名称
     * @return
     */
    protected String getLoginName() {
        return loginName;
    }
    /**
     * 获取登录用户密码
     * @return
     */
    protected String getPassword() {
        return password;
    }
    /**
     * 获取远程服务名称(主要用于记录日志)
     * @return
     */
    protected String getServiceName() {
        return serviceName;
    }
    /**
     * 设置远程服务名称(主要用于记录日志)
     * @param serviceName
     */
    protected void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }
    protected String getClientAddress() {
        try {
            this.clientAddress = RemoteServer.getClientHost();
        } catch (ServerNotActiveException e) {
            e.printStackTrace();
        }
        return clientAddress;
    }
    /**
     * 获取用户登录状态。
     * @return
     */
    protected boolean getLoginStatus() {
        return loginStatus;
    }
    /**
     * 获取用户登录状态信息。
     * @return
     */
    protected String getLoginStatusMessage() {
        return loginStatusMessage;
    }
    /**
     * 获取当前数据库名称。
     * @return
     */
    public String getGraphName() {
        return graphName;
    }
    /**
     * 获取当前数据库路径。
     * @return
     */
    public String getGraphPath() {
        return graphPath;
    }
    
    /**
     * 检查远程调用方法的权限
     * @param methodName
     */
    protected void checkRemoteMethodPermission(String methodName) throws RemoteException {
        boolean hasPermission = RemoteUserPermissionUtil.isCanAccessServiceMethod(this.getLoginName(), this.getServiceName(), methodName);
        if(hasPermission) {
            String log = String.format("%s\t来自【%s】的用户【%s】调用%s中的接口方法【%s】一次!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName(), methodName);
            RemoteServerFactory.addLog(log);
        } else {
            String log = String.format("警告:来自【%s】的用户【%s】无权调用%s中的接口方法【%s】!", this.getClientAddress(), this.getLoginName(), this.getServiceName(), methodName);
            RemoteServerFactory.addLog(log);
            throw new RemoteException(log);
        }
    }
    
    @Override
    public String remoteTest() throws RemoteException {
        return RemoteClientFactory.REMOTE_CALL_STATUS_SUCCESS;
    }

    @Override
    public int login(String loginName, String password) throws RemoteException {
        this.loginName = loginName;
        this.password = password;
        RemoteServerFactory.addLog(String.format("%s\t来自【%s】的用户【%s】正在验证访问【%s】服务!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName()));
        int loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_SUCCESS;
        if(RemoteHostUtil.isAllowHostAccess(this.getClientAddress())) {
            if(StringUtils.isBlank(loginName) || StringUtils.isBlank(password)) {
                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_PASSWORD_EMPTY;
            } else {
                if(RemoteUserUtil.getRemoteUserList().containsKey(loginName)) {
                    if(RemoteUserUtil.getRemoteUserList().get(loginName).isEnabled()) {
                        RemoteUser user = RemoteUserUtil.getRemoteUserList().get(loginName);
                        if(user.getPassword().equals(password)) {
                            if(RemoteUserPermissionUtil.isCanAccessService(loginName, this.getServiceName())) {
                                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_SUCCESS;
                                
                                if(RemoteUserUtil.getOnlineRemoteHostList().containsKey(this.getClientAddress())) {
                                    RemoteUserUtil.getOnlineRemoteHostList().remove(this.getClientAddress());
                                }
                                RemoteUserUtil.getOnlineRemoteHostList().put(this.getClientAddress(), DateTimeUtil.getFormatDateTime(new Date()));
                                
                                if(RemoteUserUtil.getOnlineRemoteUserList().containsKey(this.getLoginName())) {
                                    RemoteUserUtil.getOnlineRemoteUserList().remove(this.getLoginName());
                                }
                                RemoteUserUtil.getOnlineRemoteUserList().put(this.getLoginName(), DateTimeUtil.getFormatDateTime(new Date()));
                            } else {
                                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_PERMISSION_ERROR;
                            }
                        } else {
                            loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_PASSWORD_ERROR;
                        }
                    } else {
                        loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_ERROR;
                    }
                } else {
                    loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_ERROR;
                }
            }
        } else {
            loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_IP_PERMISSION_ERROR;
        }
        
        this.loginStatusMessage = RemoteClientFactory.parseLoginStatusValue(loginStatusValue);
        if(loginStatusValue == 0) {
            this.loginStatus = true;
            RemoteServerFactory.addLog(String.format("%s\t来自【%s】的用户【%s】通过【%s】服务的访问验证!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName()));
        } else {
            this.loginStatus = false;
            RemoteServerFactory.addLog(String.format("%s\t来自【%s】的用户【%s】未通过【%s】服务的访问验证。\r\n%s", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName(), this.getLoginStatusMessage()));
        }
        return loginStatusValue;
    }

    @Override
    public void initGraphName(String graphName) throws RemoteException {
        this.graphName = graphName;
    }
    
    @Override
    public void initGraphPath(String graphPath) throws RemoteException {
        this.graphPath = graphPath;
    }
}
3.2.3 Neo4j实体映射转换

Neo4j实体映射转换,即将Neo4j原生的接口类对象映射转换为我们在RMI Client中自定义的可序列化的远程服务实体类。主要转换方法如下所示。

/**
     * 将Node转换为GNode对象。<br>
     * <b>【备注:未进行事务处理】</b>
     * @param node
     * @return
     */
    protected GNode convertNodeToGNode(Node node) {
        if(node == null) {
            return null;
        }
        
        GNode gnode = new GNode();
        gnode.setId(node.getId());
        gnode.setDegree(node.getDegree());
        
        Iterator<Label> itLabel = node.getLabels().iterator();
        while(itLabel.hasNext()) {
            Label label = itLabel.next();
            gnode.getLabelNameList().add(label.name());
        }
        
        for(String name : node.getAllProperties().keySet()) {
            Object value = node.getProperty(name);
            gnode.getPropertyList().put(name, value);
        }
        
        Iterator<Relationship> itRelationship = node.getRelationships().iterator();
        while(itRelationship.hasNext()) {
            Relationship rel = itRelationship.next();
            String relType = rel.getType().name();
            gnode.getRelationshipList().add(rel.getId());
            if(gnode.getRelationshipTypeList().containsKey(relType)) {
                gnode.getRelationshipTypeList().get(relType).add(rel.getId());
            } else {
                ArrayList<Long> list = new ArrayList<Long>();
                list.add(rel.getId());
                gnode.getRelationshipTypeList().put(relType, list);
            }
        }
        
        Iterator<Relationship> itRelationshipIncoming = node.getRelationships(Direction.INCOMING).iterator();
        ArrayList<Long> incomingList = new ArrayList<Long>();
        while(itRelationshipIncoming.hasNext()) {
            Relationship rel = itRelationshipIncoming.next();
            incomingList.add(rel.getId());
        }
        gnode.getRelationshipDirectionList().put(Direction.INCOMING.name(), incomingList);
        
        Iterator<Relationship> itRelationshipOutgoing = node.getRelationships(Direction.OUTGOING).iterator();
        ArrayList<Long> outgoingList = new ArrayList<Long>();
        while(itRelationshipOutgoing.hasNext()) {
            Relationship rel = itRelationshipOutgoing.next();
            outgoingList.add(rel.getId());
        }
        gnode.getRelationshipDirectionList().put(Direction.OUTGOING.name(), outgoingList);
        return gnode;
    }
    
    /**
     * 将Node对象转换为GNode对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param nodes
     * @return 
     */
    public List<GNode> parseNodes(List<Node> nodes) {
        List<GNode> gNodeList = new ArrayList<GNode>();
        if(nodes == null || nodes.size() == 0) return gNodeList;
        if(this.getTemplate() == null) return gNodeList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(Node node : nodes) {
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gNodeList.add(gnode);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gNodeList;
    }
    
    /**
     * 将Node对象转换为GNode对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param node
     * @return 
     */
    public GNode parseNode(Node node) {
        List<Node> nodes = Arrays.asList(node);
        List<GNode> gNodeList = this.parseNodes(nodes);
        if(gNodeList == null || gNodeList.size() == 0) {
            return null;
        } else {
            return gNodeList.get(0);
        }
    }
    
    /**
     * 根据编码解析节点对象,将其转为GNode对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param nodeIDs
     * @return
     */
    public List<GNode> parseNodesByID(List<Long> nodeIDs) {
        List<GNode> gNodeList = new ArrayList<GNode>();
        if(nodeIDs == null || nodeIDs.size() == 0) return gNodeList;
        if(this.getTemplate() == null) return gNodeList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(long nodeID : nodeIDs) {
                Node node = this.getTemplate().getGraphDBService().getNodeById(nodeID);
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gNodeList.add(gnode);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gNodeList;
    }
    
    /**
     * 根据编码解析节点对象,将其转为GNode对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param nodeID
     * @return
     */
    public GNode parseNodeByID(long nodeID) {
        List<Long> nodeIDs = Arrays.asList(nodeID);
        List<GNode> gNodeList = this.parseNodesByID(nodeIDs);
        if(gNodeList == null || gNodeList.size() == 0) {
            return null;
        } else {
            return gNodeList.get(0);
        }
    }
    
    /**
     * 将Relationship转换为GRelationship对象。<br>
     * <b>【备注:未进行事务处理】</b>
     * @param relationship
     * @return
     */
    protected GRelationship convertRelToGRel(Relationship relationship) {
        if(relationship == null) {
            return null;
        }
        
        GRelationship grelationship = new GRelationship();
        grelationship.setId(relationship.getId());
        grelationship.setStartNodeID(relationship.getStartNode().getId());
        grelationship.setEndNodeID(relationship.getEndNode().getId());
        grelationship.setRelationshipType(relationship.getType().name());
        for(String name : relationship.getAllProperties().keySet()) {
            Object value = relationship.getProperty(name);
            grelationship.getPropertyList().put(name, value);
        }
        return grelationship;
    }
    
    /**
     * 将Relationship对象转换为GRelationship对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param relationships
     * @return 
     */
    public List<GRelationship> parseRelationships(List<Relationship> relationships) {
        List<GRelationship> gRelationshipList = new ArrayList<GRelationship>();
        if(relationships == null || relationships.size() == 0) return gRelationshipList;
        if(this.getTemplate() == null) return gRelationshipList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(Relationship relationship : relationships) {
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gRelationshipList.add(grelationship);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gRelationshipList;
    }
    
    /**
     * 将Relationship对象转换为GRelationship对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param relationship
     * @return 
     */
    public GRelationship parseRelationship(Relationship relationship) {
        List<Relationship> relationships = Arrays.asList(relationship);
        List<GRelationship> gRelationshipList = this.parseRelationships(relationships);
        if(gRelationshipList == null || gRelationshipList.size() == 0) {
            return null;
        } else {
            return gRelationshipList.get(0);
        }
    }
    
    /**
     * 根据编码解析关系对象,将其转为GRelationship对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param relationshipIDs
     * @return
     */
    public List<GRelationship> parseRelationshipsByID(List<Long> relationshipIDs) {
        List<GRelationship> gRelationshipList = new ArrayList<GRelationship>();
        if(relationshipIDs == null || relationshipIDs.size() == 0) return gRelationshipList;
        if(this.getTemplate() == null) return gRelationshipList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(long relationshipID : relationshipIDs) {
                Relationship relationship = this.getTemplate().getGraphDBService().getRelationshipById(relationshipID);
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gRelationshipList.add(grelationship);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gRelationshipList;
    }
    
    /**
     * 根据编码解析关系对象,将其转为GRelationship对象。<br>
     * <b>【备注:已进行事务处理】</b>
     * @param relationshipID
     * @return
     */
    public GRelationship parseRelationshipByID(long relationshipID) {
        List<Long> relationshipIDs = Arrays.asList(relationshipID);
        List<GRelationship> gRelationshipList = this.parseRelationshipsByID(relationshipIDs);
        if(gRelationshipList == null || gRelationshipList.size() == 0) {
            return null;
        } else {
            return gRelationshipList.get(0);
        }
    }
    
    /**
     * 将Path对象转换为GPath对象。<br>
     * <b>【备注:未进行事务处理】</b>
     * @param paths
     * @return
     */
    protected List<GPath> convertPathToGPath(List<Path> paths) {
        List<GPath> gPathList = new ArrayList<GPath>();
        if(paths == null || paths.size() == 0) return gPathList;
        
        for(Path path : paths) {
            GPath gpath = new GPath();
            
            Iterator<Node> itNode = path.nodes().iterator();
            while(itNode.hasNext()) {
                Node node = itNode.next();
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gpath.getNodes().add(gnode);
            }
            
            Iterator<Relationship> itRelationship = path.relationships().iterator();
            while(itRelationship.hasNext()) {
                Relationship relationship = itRelationship.next();
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gpath.getRelationships().add(grelationship);
            }
            
            gPathList.add(gpath);
        }
        
        return gPathList;
    }
    
    /**
     * 将Path对象转换为GPath对象。<br>
     * <b>【备注:未进行事务处理】</b>
     * @param path
     * @return
     */
    protected GPath convertPathToGPath(Path path) {
        List<Path> paths = Arrays.asList(path);
        List<GPath> gPathList = this.convertPathToGPath(paths);
        if(gPathList == null || gPathList.size() == 0) {
            return null;
        } else {
            return gPathList.get(0);
        }
    }
    
    /**
     * 将GPage对象转换为GraphPageModel对象。
     * @param gpage
     * @return
     */
    public GraphPageModel parseGPageToGraphPageModel(GPage gpage) {
        GraphPageModel pageModel = null;
        if(gpage == null) {
            pageModel = new GraphPageModel(20);
        } else {
            pageModel = new GraphPageModel(gpage.getPageSize());
            pageModel.setPageIndex(gpage.getPageIndex());
            pageModel.setTotalCount(gpage.getTotalCount());
        }
        return pageModel;
    }
    
    /**
     * 将GraphPageModel对象转换为GPage对象。
     * @param pageModel
     * @return
     */
    public GPage parseGraphPageModelToGPage(GraphPageModel pageModel) {
        GPage gpage = null;
        if(pageModel == null) {
            gpage = new GPage(20);
        } else {
            gpage = new GPage(pageModel.getPageSize());
            gpage.setPageIndex(pageModel.getPageIndex());
            gpage.setTotalCount(pageModel.getTotalCount());
            List<GNode> nodeList = this.parseNodes(pageModel.getNodeList());
            for(GNode node : nodeList) {
                gpage.getNodeList().add(node);
            }
            List<GRelationship> relationshipList = this.parseRelationships(pageModel.getRelationshipList());
            for(GRelationship relationship : relationshipList) {
                gpage.getRelationshipList().add(relationship);
            }
        }
        return gpage;
    }
    
    /**
     * 将GDirection对象转换为Direction对象。
     * @param gdirection
     * @return
     */
    public Direction parseGDirection(GDirection gDirection) {
        if(gDirection == null) {
            return null;
        }
        if(StringUtils.isBlank(gDirection.getName())) {
            return null;
        }
        return Direction.valueOf(gDirection.getName());
    }
    
    /**
     * 将关系类型名称解析为关系类型枚举对象。
     * @param relationshipType
     * @return
     */
    public RelationshipType parseRelationshipType(String relationshipType) {
        if(StringUtils.isBlank(relationshipType)) {
            return null;
        }
        return this.getTemplate().getRelTypeUtil().get(relationshipType);
    }

 

3.3. RMI Server Form

RMI Server Form,即RMI服务窗口管理器,是通过窗口方式来管理RMI服务,包括启动RMI服务,停止RMI服务,监控RMI日志信息,监控远程登录用户信息,初始化相关配置等操作。此功能集成在“图数据库管理系统Server端”,功能界面如下图所示:

remote service

 

【完】

作者:商兵兵

单位:河南省电力科学研究院智能电网所

QQ:52190634

主页:http://www.cnblogs.com/shangbingbing

空间:http://shangbingbing.qzone.qq.com

转载于:https://www.cnblogs.com/shangbingbing/p/5028791.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值