springboot整合mybatis-plus打印完整SQL语句

mybatis-plus控制台打印完整带参数SQL语句

问题背景

通常我们开发的时候,需要联合控制台和Navicat/PLSQL等工具进行语句的拼接检查,如果只是输出了一堆???,那么将极大降低我们的效率。因此我们需要输出完整的SQL语句以便调试。

解决方案

1.Mybatis拦截器实现统计sql执行时间及打印完整sql语句

代码

import java.sql.Connection;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.stereotype.Component;

/**
 * 拦截StatementHandler类中参数类型为Statement的 prepare 方法
 * 即拦截 Statement prepare(Connection var1, Integer var2) 方法

 */

@Component
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisSqlLoggerInterceptor implements Interceptor {


	@Override
	public Object intercept(Invocation invocation) throws Throwable {

		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		//通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
		//优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
		MetaObject metaObject = MetaObject
				.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
						new DefaultReflectorFactory());
		//先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		//id为执行的mapper方法的全路径名,如com.uv.dao.UserMapper.insertUser
		String id = mappedStatement.getId();
		log.info("id ==> " + id);
		//sql语句类型 select、delete、insert、update
		String sqlCommandType = mappedStatement.getSqlCommandType().toString();
		log.info("类型 ==> " + sqlCommandType);

		BoundSql boundSql = statementHandler.getBoundSql();
      
		// 获取节点的配置
		Configuration configuration = mappedStatement.getConfiguration();
		// 获取到最终的sql语句
		String newsql = getSql(configuration, boundSql, id);
		log.info("拦截的sql ==>: " + newsql);
		long start = System.currentTimeMillis();
		Object returnValue = invocation.proceed();
		long end = System.currentTimeMillis();
		long time = (end - start);
		log.info("sql耗时 ==>: " + time);
		return returnValue;
		//return null;
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
	}


	/**
	 * 封装了一下sql语句,
	 * 使得结果返回完整xml路径下的sql语句节点id + sql语句
	 *
	 * @param configuration
	 * @param boundSql
	 * @param sqlId
	 * @return
	 */
	private String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
		String sql = showSql(configuration, boundSql);
		StringBuilder str = new StringBuilder(100);
		str.append(sqlId);
		str.append(":");
		str.append(sql);
		return str.toString();
	}

	/**
	 * 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号;
	 * 对参数是null和不是null的情况作了处理<br>
	 *
	 * @param obj
	 * @return
	 */
	private String getParameterValue(Object obj) {
		String value = null;
		if (obj instanceof String) {
			value = "'" + obj.toString() + "'";
		} else if (obj instanceof Date) {
			DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
			value = "'" + formatter.format(new Date()) + "'";
		} else {
			if (obj != null) {
				value = obj.toString();
			} else {
				value = "";
			}

		}
		return value;
	}

	/**
	 * 进行?的替换
	 * @param configuration
	 * @param boundSql
	 * @return
	 */
	public String showSql(Configuration configuration, BoundSql boundSql) {
		// 获取参数
		Object parameterObject = boundSql.getParameterObject();
		List<ParameterMapping> parameterMappings = boundSql
				.getParameterMappings();
		// sql语句中多个空格都用一个空格代替
		String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
		if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {
			// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换       
			// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
				sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));

			} else {
				//MetaObject主要是封装了originalObject对象,
				// 提供了get和set的方法用于获取和设置originalObject的属性值,
				// 主要支持对JavaBean、Collection、Map三种类型对象的操作
				MetaObject metaObject = configuration.newMetaObject(parameterObject);
				for (ParameterMapping parameterMapping : parameterMappings) {
					String propertyName = parameterMapping.getProperty();
					if (metaObject.hasGetter(propertyName)) {
						Object obj = metaObject.getValue(propertyName);
						sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						// 该分支是动态sql
						Object obj = boundSql.getAdditionalParameter(propertyName);
						sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));

					} else {
						//打印出缺失,提醒该参数缺失并防止错位
						sql = sql.replaceFirst("\\?", "缺失");
					}
				}
			}
		}
		return sql;
	}

}

打印的sql

c.p.p.a.i.MybatisSqlLoggerInterceptor    : 拦截的sql ==>: com.liu.test.admin.mapper.SysDictMapper.selectById:SELECT id,type,description,create_time,update_time,`system`,remarks,del_flag FROM sys_dict WHERE id=3 AND del_flag='0'
2021-07-06 16:00:46.463  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : sql耗时 ==>: 103
2021-07-06 16:00:46.788  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : id ==> com.liu.test.admin.mapper.SysDictMapper.updateById
2021-07-06 16:00:46.790  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : 类型 ==> UPDATE
2021-07-06 16:00:46.793  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : 拦截的sql ==>: com.liu.test.admin.mapper.SysDictMapper.updateById:UPDATE sys_dict SET type='22', description='ssfd55622', create_time=2021-07-06T13:52:55, update_time=2021-07-06T13:52:55, `system`='0', remarks='dsdfd5562247788' WHERE id=3 AND del_flag='0'
2021-07-06 16:00:46.795  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : sql耗时 ==>: 0
2021-07-06 16:00:47.830  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : id ==> com.liu.test.admin.mapper.SysLogMapper.insert
2021-07-06 16:00:47.836  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : 类型 ==> INSERT
2021-07-06 16:00:47.842  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : 拦截的sql ==>: com.liu.test.admin.mapper.SysLogMapper.insert:INSERT INTO sys_log ( type, title, create_by, remote_addr, user_agent, request_uri, method, params, time, service_id ) VALUES ( '0', '修改字典', 'admin', '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', '/dict', 'PUT', '', 1754, 'pig' )
2021-07-06 16:00:47.846  INFO 27608 --- [  XNIO-1 task-1] c.p.p.a.i.MybatisSqlLoggerInterceptor    : sql耗时 ==>: 2

2.p6spy 方式

引入依赖

<!-- https://mvnrepository.com/artifact/p6spy/p6spy -->
		<dependency>
			<groupId>p6spy</groupId>
			<artifactId>p6spy</artifactId>
			<version>3.9.0</version>
		</dependency>

修改配置

# 数据源
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    username: root
    password: root
    url: jdbc:p6spy:mysql://pig-mysql:3307/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai

添加配置spy.properties

# SQl语句分析插件配置

modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

打印的日志

Consume Time:76 ms 2021-07-06 16:50:29
 Execute SQL:INSERT INTO sys_dict ( type, description, `system`, remarks ) VALUES ( 'tt', 'tt', '0', 'ttt' )

 Consume Time:46 ms 2021-07-06 16:50:30
 Execute SQL:INSERT INTO sys_log ( type, title, create_by, remote_addr, user_agent, request_uri, method, params, time, service_id ) VALUES ( '0', '添加字典', 'admin', '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', '/dict', 'POST', '', 3023, 'pig' )

 Consume Time:8 ms 2021-07-06 16:50:30
 Execute SQL:SELECT COUNT(*) FROM sys_dict WHERE del_flag = '0'

 Consume Time:6 ms 2021-07-06 16:50:30
 Execute SQL:SELECT id,type,description,create_time,update_time,`system`,remarks,del_flag FROM sys_dict WHERE del_flag='0' LIMIT 20

3.修改MybatisParameterHandler 源码

配置

#mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

重写com.baomidou.mybatisplus.core.MybatisParameterHandler类的****setParameters**** 方法

源码如下

package com.baomidou.mybatisplus.core;
/*
 * Copyright (c) 2011-2021, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;

/**
 * 自定义 ParameterHandler 重装构造函数,填充插入方法主键 ID
 *
 * @author nieqiuqiu 2020/6/5
 * @since 3.4.0
 */
public class MybatisParameterHandler implements ParameterHandler {

	private final TypeHandlerRegistry typeHandlerRegistry;
	private final MappedStatement mappedStatement;
	private final Object parameterObject;
	private final BoundSql boundSql;
	private final Configuration configuration;
	private final SqlCommandType sqlCommandType;

	public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
		this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
		this.mappedStatement = mappedStatement;
		this.boundSql = boundSql;
		this.configuration = mappedStatement.getConfiguration();
		this.sqlCommandType = mappedStatement.getSqlCommandType();
		this.parameterObject = processParameter(parameter);
	}

	public Object processParameter(Object parameter) {
		/* 只处理插入或更新操作 */
		if (parameter != null
				&& (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
			//检查 parameterObject
			if (ReflectionKit.isPrimitiveOrWrapper(parameter.getClass())
					|| parameter.getClass() == String.class) {
				return parameter;
			}
			Collection<Object> parameters = getParameters(parameter);
			if (null != parameters) {
				// 感觉这里可以稍微优化一下,理论上都是同一个.
				parameters.forEach(this::process);
			} else {
				process(parameter);
			}
		}
		return parameter;
	}

	@Override
	public Object getParameterObject() {
		return this.parameterObject;
	}

	private void process(Object parameter) {
		if (parameter != null) {
			TableInfo tableInfo = null;
			Object entity = parameter;
			if (parameter instanceof Map) {
				Map<?, ?> map = (Map<?, ?>) parameter;
				if (map.containsKey(Constants.ENTITY)) {
					Object et = map.get(Constants.ENTITY);
					if (et != null) {
						entity = et;
						tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
					}
				}
			} else {
				tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
			}
			if (tableInfo != null) {
				//到这里就应该转换到实体参数对象了,因为填充和ID处理都是争对实体对象处理的,不用传递原参数对象下去.
				MetaObject metaObject = this.configuration.newMetaObject(entity);
				if (SqlCommandType.INSERT == this.sqlCommandType) {
					populateKeys(tableInfo, metaObject, entity);
					insertFill(metaObject, tableInfo);
				} else {
					updateFill(metaObject, tableInfo);
				}
			}
		}
	}


	protected void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
		final IdType idType = tableInfo.getIdType();
		final String keyProperty = tableInfo.getKeyProperty();
		if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
			final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(this.configuration).getIdentifierGenerator();
			Object idValue = metaObject.getValue(keyProperty);
			if (StringUtils.checkValNull(idValue)) {
				if (idType.getKey() == IdType.ASSIGN_ID.getKey()) {
					if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
						metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
					} else {
						metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
					}
				} else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) {
					metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
				}
			}
		}
	}


	protected void insertFill(MetaObject metaObject, TableInfo tableInfo) {
		GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
			if (metaObjectHandler.openInsertFill() && tableInfo.isWithInsertFill()) {
				metaObjectHandler.insertFill(metaObject);
			}
		});
	}

	protected void updateFill(MetaObject metaObject, TableInfo tableInfo) {
		GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
			if (metaObjectHandler.openUpdateFill() && tableInfo.isWithUpdateFill()) {
				metaObjectHandler.updateFill(metaObject);
			}
		});
	}

	/**
	 * 处理正常批量插入逻辑
	 * <p>
	 * org.apache.ibatis.session.defaults.DefaultSqlSession$StrictMap 该类方法
	 * wrapCollection 实现 StrictMap 封装逻辑
	 * </p>
	 *
	 * @return 集合参数
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	protected Collection<Object> getParameters(Object parameterObject) {
		Collection<Object> parameters = null;
		if (parameterObject instanceof Collection) {
			parameters = (Collection) parameterObject;
		} else if (parameterObject instanceof Map) {
			Map parameterMap = (Map) parameterObject;
			if (parameterMap.containsKey("collection")) {
				parameters = (Collection) parameterMap.get("collection");
			} else if (parameterMap.containsKey("list")) {
				parameters = (List) parameterMap.get("list");
			} else if (parameterMap.containsKey("array")) {
				parameters = Arrays.asList((Object[]) parameterMap.get("array"));
			}
		}
		return parameters;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void setParameters(PreparedStatement ps) {
		ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
		List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
		if (parameterMappings != null) {
			String sql = boundSql.getSql();
			List<Object> parameters = new ArrayList<>();
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					if (this.boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
						value = this.boundSql.getAdditionalParameter(propertyName);
					} else if (this.parameterObject == null) {
						value = null;
					} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
						value = parameterObject;
					} else {
						MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
						value = metaObject.getValue(propertyName);
					}
					TypeHandler typeHandler = parameterMapping.getTypeHandler();
					JdbcType jdbcType = parameterMapping.getJdbcType();
					if (value == null && jdbcType == null) {
						jdbcType = this.configuration.getJdbcTypeForNull();
					}
					try {
						typeHandler.setParameter(ps, i + 1, value, jdbcType);
						parameters.add(value);
					} catch (TypeException | SQLException e) {
						throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
					}


				}
			}


			try {
				sql = handleListParameter(sql, parameters);
				System.out.println("输出完整的sql-------------------->>>" + sql);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}


	/**
	 * 是否基本数据类型或者基本数据类型的包装类
	 */
	private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {
		return parameterObjectClass.isPrimitive() ||
				(parameterObjectClass.isAssignableFrom(BigDecimal.class) || parameterObjectClass.isAssignableFrom(Byte.class) || parameterObjectClass.isAssignableFrom(Short.class) ||
						parameterObjectClass.isAssignableFrom(Integer.class) || parameterObjectClass.isAssignableFrom(Long.class) ||
						parameterObjectClass.isAssignableFrom(Double.class) || parameterObjectClass.isAssignableFrom(Float.class) ||
						parameterObjectClass.isAssignableFrom(Character.class) || parameterObjectClass.isAssignableFrom(Boolean.class));
	}

	/**
	 * 参数转换拼接替换
	 * @param sql
	 * @param col
	 * @return
	 */
	private String handleListParameter(String sql, Collection<?> col) {
		if (col != null && col.size() != 0) {
			for (Object obj : col) {
				String value = null;
				Class<?> objClass = obj.getClass();

				// 类型匹配输出
				if (isPrimitiveOrPrimitiveWrapper(objClass)) {
					value = obj.toString();
				} else if (objClass.isAssignableFrom(String.class)) {
					value = "\'" + obj.toString() + "\'";
				} else if (objClass.isAssignableFrom(Date.class)) {
					try {
						value = cn.hutool.core.date.DateUtil.format((Date) obj, "yyyy-MM-dd HH:mm:ss");
						value = "to_date('" + value + "','yyyy-mm-dd hh24:mi:ss')";
					} catch (Exception e) {
						e.printStackTrace();
						value = "\'" + obj.toString() + "\'";

					}
				} else {
					//暂定吧
					value = "\'" + obj.toString() + "\'";
				}

				sql = sql.replaceFirst("\\?", value);

			}

		}

		return sql;
	}

}


打印日志

输出完整的sql-------------------->>>INSERT INTO sys_dict ( type,
description,

system,
remarks ) VALUES ( ‘rr’,
‘ggg’,

‘0’,
‘rrr’ )

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值