`

通过jdbc实现Oracle备份到Mysql

阅读更多

一直用的系统是 Oracle的数据库,现在一个客户比较小,不想投入钱购买数据库软件,所以只能使用Mysql,需要将框架中的一些表结构、记录从Oracle导入到Mysql中去。网上没找到好用的软件,所以决定自己弄一个小程序实现异构库备份:

  思路比较简单, 先生成建表语句并在mysql库中执行,在生成insert语句在mysql中执行。

         一: 生成建表语句重点是需要获取表的字段,默认值,是否为空, 字段类型、字段长度等, 万幸的是这些jdbc都有实现,通过Connect获取数据库的DatabaseMetaData--dmd

 

               System.out.println("加载Oracle链接0...");
		connect = connect(0);
		
		System.out.println("加载Mysql链接1...");
		insertConnect = connect(1);
		
		System.out.println("获取dmd...");
		dmd = connect.getMetaData();

 

 

 

再通过DatabaseMetaData--dmd查询表单字段信息,

 

ResultSet colrs = dmd.getColumns("%", dmd.getUserName(),table.toUpperCase(), "%");

 然后解析获取到的ResultSet信息后即可获取到字段的全部信息,生成建表语句:

 

 

 

    二取生成insert语句类似,需要适用select语句查询表的信息,然后循环解析每一行的记录,生成insert插入,此处执行sql的是PreparedStatement,可以循环适用insert语句,仅需要改变每一次动态设置的值属性即可。

 

 

全部代码比较简单,直接上代码:

有两个类:jdbc工具类:JDBCUtil.java

 

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class JDBCUtil {

	public static Connection connect(int type) {
		Connection conn;

		if (type == 0) {//oracle
			conn = connectOracle();
		} else {		//Mysql
			conn = connectMysql();
		}

		return conn;
	}

	/**
	 * 连接Oracle
	 * 
	 * @return
	 */
	public static Connection connectMysql() {

		// 连接MySql数据库,用户名和密码
		String username = "111212";
		String password = "2121";
		String url = "jdbc:mysql://192.168.1.1/training?user=" + username+ "&password=" + password + "";
		Connection conn = null;

		try {
			// 加载MySql的驱动类
			Class.forName("com.mysql.jdbc.Driver");
			conn = DriverManager.getConnection(url);
		} catch (ClassNotFoundException e) {
			System.out.println("找不到驱动程序类 ,加载驱动失败!");
			e.printStackTrace();
		} catch (SQLException se) {
			System.out.println("数据库连接失败!");
			se.printStackTrace();
		}

		return conn;
	}

	/**
	 * 连接Oracle
	 * 
	 * @return
	 */
	public static Connection connectOracle() {
		String url = "jdbc:oracle:thin:@192.168.1.1:1521:orcl";

		// 连接MySql数据库,用户名和密码都是root
		String username = "abcaf";
		String password = "syjgagagac";
		Connection conn = null;

		try {
			// 加载MySql的驱动类
			Class.forName("oracle.jdbc.OracleDriver");
			conn = DriverManager.getConnection(url, username, password);
		} catch (ClassNotFoundException e) {
			System.out.println("找不到驱动程序类 ,加载驱动失败!");
			e.printStackTrace();
		} catch (SQLException se) {
			System.out.println("数据库连接失败!");
			se.printStackTrace();
		}

		return conn;
	}
	
	
	public static List getColumnNames(ResultSetMetaData meta) throws SQLException{
		List list = new ArrayList();
		for (int i = 1; i <= meta.getColumnCount(); i++) {
			list.add(meta.getColumnName(i));
		}
		return list;
	}
	

	// ResultSetMetaData 使用示例
	// 此方法参考 http://blog.csdn.net/yirentianran/article/details/2950321
	public static void demoResultSetMetaData(ResultSetMetaData data)
			throws SQLException {
		for (int i = 1; i <= data.getColumnCount(); i++) {
			// 获得所有列的数目及实际列数
			int columnCount = data.getColumnCount();
			// 获得指定列的列名
			String columnName = data.getColumnName(i);
			// 获得指定列的列值
			// String columnValue = rs.getString(i);
			// 获得指定列的数据类型
			int columnType = data.getColumnType(i);
			// 获得指定列的数据类型名
			String columnTypeName = data.getColumnTypeName(i);
			// 所在的Catalog名字
			String catalogName = data.getCatalogName(i);
			// 对应数据类型的类
			String columnClassName = data.getColumnClassName(i);
			// 在数据库中类型的最大字符个数
			int columnDisplaySize = data.getColumnDisplaySize(i);
			// 默认的列的标题
			String columnLabel = data.getColumnLabel(i);
			// 获得列的模式
			String schemaName = data.getSchemaName(i);
			// 某列类型的精确度(类型的长度)
			int precision = data.getPrecision(i);
			// 小数点后的位数
			int scale = data.getScale(i);
			// 获取某列对应的表名
			String tableName = data.getTableName(i);
			// 是否自动递增
			boolean isAutoInctement = data.isAutoIncrement(i);
			// 在数据库中是否为货币型
			boolean isCurrency = data.isCurrency(i);
			// 是否为空
			int isNullable = data.isNullable(i);
			// 是否为只读
			boolean isReadOnly = data.isReadOnly(i);
			// 能否出现在where中
			boolean isSearchable = data.isSearchable(i);
			System.out.println(columnCount);
			System.out.println("获得列" + i + "的字段名称:" + columnName);
			// System.out.println("获得列" + i + "的字段值:" + columnValue);
			System.out.println("获得列" + i + "的类型,返回SqlType中的编号:" + columnType);
			System.out.println("获得列" + i + "的数据类型名:" + columnTypeName);
			System.out.println("获得列" + i + "所在的Catalog名字:" + catalogName);
			System.out.println("获得列" + i + "对应数据类型的类:" + columnClassName);
			System.out.println("获得列" + i + "在数据库中类型的最大字符个数:"
					+ columnDisplaySize);
			System.out.println("获得列" + i + "的默认的列的标题:" + columnLabel);
			System.out.println("获得列" + i + "的模式:" + schemaName);
			System.out.println("获得列" + i + "类型的精确度(类型的长度):" + precision);
			System.out.println("获得列" + i + "小数点后的位数:" + scale);
			System.out.println("获得列" + i + "对应的表名:" + tableName);
			System.out.println("获得列" + i + "是否自动递增:" + isAutoInctement);
			System.out.println("获得列" + i + "在数据库中是否为货币型:" + isCurrency);
			System.out.println("获得列" + i + "是否为空:" + isNullable);
			System.out.println("获得列" + i + "是否为只读:" + isReadOnly);
			System.out.println("获得列" + i + "能否出现在where中:" + isSearchable);
		}
	}

	// 演示 DatabaseMetaData
	public static void demoDatabaseMetaData() {
		try {
			Connection con = connect(0);
			//
			DatabaseMetaData dmd = con.getMetaData();
			System.out.println("当前数据库是:" + dmd.getDatabaseProductName());
			System.out.println("当前数据库版本:" + dmd.getDatabaseProductVersion());
			System.out.println("当前数据库驱动:" + dmd.getDriverVersion());
			System.out.println("当前数据库URL:" + dmd.getURL());
			System.out.println("当前数据库是否是只读模式?:" + dmd.isReadOnly());
			System.out.println("当前数据库是否支持批量更新?:" + dmd.supportsBatchUpdates());
			System.out .println("当前数据库是否支持结果集的双向移动(数据库数据变动不在ResultSet体现)?:" + dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE));
			System.out .println("当前数据库是否支持结果集的双向移动(数据库数据变动会影响到ResultSet的内容)?:" + dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE));
			System.out.println("========================================");

			// ResultSet rs = dmd.getTables(null, null, "%", null);
			// System.out.println("表名" + "," + "表类型");
			/*
			 * while (rs.next()) { String tname =
			 * rs.getString("TABLE_NAME").toLowerCase();
			 * if(!tname.startsWith("t_")){ continue; }
			 * System.out.println(rs.getString("TABLE_NAME") + ","+
			 * rs.getString("TABLE_TYPE")); }
			 */
			System.out.println("========================================");

			ResultSet rs = dmd.getPrimaryKeys(null, "SYJC", "t_mz_djlr".toUpperCase());
			while (rs.next()) {
				System.out .println(rs.getString(3) + "表的主键是:" + rs.getString(4));
			}
			System.out.println("========================================");

			rs = dmd.getColumns("%", "SYJC", "t_mz_djlr", "%");
			System.out.println("t_student表包含的字段:");
			while (rs.next()) {
				System.out.println(rs.getString(4) + " " + rs.getString(6) + "(" + rs.getString(7) + ");");
			}
			System.out.println("========================================");

		} catch (Exception e) {
			System.out.println("数据库操作出现异常");
		}
	}

}

 

还有一个实现 解析字段信息生成建表语句、解析结果生成insert语句的类DBBackUtil.java

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DBBackUtil  extends JDBCUtil{

	public static Connection connect;
	public static Connection insertConnect;
	public static DatabaseMetaData dmd;
	public static Map dataTypesMap = new HashMap();
	public static List<String> types = new ArrayList();
	public static Map<String,Boolean> unneedLength = new HashMap();
	
	
	
	static{
		/**
		 * 此处对象的是 jdbcType  将要转换到mysql的数据类型
		 */
		dataTypesMap.put(12+"","VARCHAR");
		dataTypesMap.put(3+"","DECIMAL");
		dataTypesMap.put(93+"","TIMESTAMP");
		dataTypesMap.put(1+"","CHAR");
		dataTypesMap.put(6+"","DECIMAL");
		dataTypesMap.put(2005+"","LONGTEXT");
		dataTypesMap.put(1111+"","VARCHAR");
		
		/***不需要长度的字段, clob(longtext)、TIMESTAMP*/
		unneedLength.put(93+"",true);
		unneedLength.put(2005+"",true);
		
		types.add("VARCHAR2");
		types.add("NUMBER");
		types.add("DATE");
		types.add("CHAR");
		types.add("FLOAT");
		types.add("CLOB");
		types.add("NCHAR");
	}
	
	

	public static void main(String[] args) throws SQLException {
		
		System.out.println("加载Oracle链接0...");
		connect = connect(0);
		
		System.out.println("加载Mysql链接1...");
		insertConnect = connect(1);
		
		System.out.println("获取dmd...");
		dmd = connect.getMetaData();

		String[] tables = { "t_base_zg" };
		
		String table = null;

		for (int j = 0; j < tables.length; j++) {
			table = tables[j];
			System.out.println("===========================================================");
			createCreateSQL(table);
			System.out.println("===========================================================");
			System.out.println("插入开始表:"+table);
			createInsertSQL(table);
			System.out.println("插入表结束:"+table);
			System.out.println("===========================================================");
		}

//		System.out.println("*******************************************");
//		for (String ty : types) {
//			System.out.println(ty);
//		}
//		System.out.println("*******************************************");
		connect.close();

	}
	
	private static void createInsertSQL(String table) throws SQLException {
		Statement stmt = createStmt(connect);
		String sql = "select * from "+table;
		System.out.println("查询表数据  "+table);
		ResultSet rs = stmt.executeQuery(sql);
		
		System.out.println("获取表字段。。。。"+table);
		List<String> columns = getColumnNames(rs.getMetaData());
		StringBuffer insertSql = new StringBuffer("INSERT INTO  "+table+"(");
		for (String col : columns) {
			insertSql.append(col+",");
		}
		insertSql.delete(insertSql.length()-1, insertSql.length());
		insertSql.append(") \n values(");
		for (String col : columns) {
			insertSql.append("?,");
		}
		insertSql.delete(insertSql.length()-1, insertSql.length());
		insertSql.append(")");
		System.out.println(insertSql.toString());
		
		System.out.println("循环插入数据......");
		PreparedStatement preStmt = null;
		while(rs.next()){
			preStmt = createPreStmt(insertConnect, insertSql.toString());
			//循环结果集 执行插入操作
			for (int i = 0; i < columns.size(); i++) {
				Object obj = rs.getObject(columns.get(i));
				
				//System.out.println(obj+"\t"+(obj!=null?obj.getClass():""));
				preStmt.setObject(i+1, obj);
			}
			
			int count = preStmt.executeUpdate();
			System.out.println("插入 "+count+"条");
			preStmt.clearBatch();
			preStmt.clearParameters();
			preStmt.close();
		}
		
		rs.close();
	}



	/**
	 * 生成建表语句
	 * @param table
	 * @return
	 * @throws SQLException
	 */
	public static  String createCreateSQL(String table) throws SQLException{
		
		ResultSet colrs = dmd.getColumns("%", dmd.getUserName(),table.toUpperCase(), "%");
		
		StringBuffer sb = new StringBuffer("create table " + table + "(\n");
		System.out.println("获取表 【"+table+"】的字段创建按create语句");
		while (colrs.next()) {
			String columnName = colrs.getString("COLUMN_NAME");	//字段名
			int dataType = colrs.getInt("DATA_TYPE");			//数据类型    数字表示
			String dataTypeName = (String) dataTypesMap.get(dataType+"");
			String typeName = colrs.getString("TYPE_NAME");		//类型名		varchar2等,数据库的实际字段类型
			if(!types.contains(typeName)){
				types.add(typeName);
			}
			int columnSize = colrs.getInt("COLUMN_SIZE");
			String remarks = colrs.getString("REMARKS");
			int nullable  = colrs.getInt("NULLABLE");
			String columnDef  = colrs.getString("COLUMN_DEF");
			sb.append("\t"+columnName.toLowerCase()+"\t");//字段名
			
			if( columnSize == 4000 && "VARCHAR".equals(dataTypeName)){
				sb.append("Text");//daatype
			}else{
				sb.append(dataTypeName);//daatype
			}
			
			Boolean unNeedLength = unneedLength.get(dataType+"");
			if(!(unNeedLength!=null && unNeedLength.booleanValue())){
				if("DECIMAL".equals(dataTypeName)){
					sb.append("("+(columnSize>10?10:columnSize));	//字段长度
				}else{
					sb.append("("+columnSize);	//字段长度
				}
				sb.append(")");
			}
			
			sb.append("\t");
			//sb.append("typeName:"+typeName+"\t");
			//sb.append("dataType:"+dataType+"\t");
			if(columnDef!=null && !"".equals(columnDef.trim())){
				sb.append("default "+columnDef+" \t"); //默认值
			}
			//sb.append("remarks:"+remarks+"\t");
			sb.append((nullable == 0 ? " not null " : "")+","); //是否为空
			sb.append("\n");
		}
		
		
		//去掉最后的逗号
		sb.replace(sb.length()-3, sb.length(), "");
		System.out.println("获取表 【"+table+"】的主键....");
		/**
		 * 生成主键
		 */
		String keySql = createKeySqlByTableName(table);
		if(keySql!=null && keySql.length()>0){
			sb.append(",\n");
			sb.append("\t"+keySql+"\n");
		}
		
		sb.append(") ENGINE=INNODB ;");
		System.out.println(sb.toString());
		
		Statement stmt = createStmt(insertConnect);
		System.out.println("执行表 【"+table+"】的建表语句 ....");
		boolean runRs = stmt.execute(sb.toString());
		System.out.println("上述建表语句 执行完成");
		
		return sb.toString();
	}
	
	
	
	
	
	
	/**
	 * 获取表主键集合
	 * @return
	 * @throws SQLException
	 */
	private static List getTableKeys(String tableName) throws SQLException{
		ResultSet  rs = dmd.getPrimaryKeys(null, "SYJC", tableName.toUpperCase());//此处旭要将表名设置成大写 oracle 只认大写
		List list = new ArrayList();
		while (rs.next()) {
			list.add(rs.getString(4).toLowerCase());
		}
		rs.close();
		return list;
	}
	
	/**
	 * 将表名拼接成Key sql
	 * @param tableName
	 * @return
	 * @throws SQLException
	 */
	public static String createKeySqlByTableName(String tableName) throws SQLException{
		
		List list = getTableKeys(tableName);
		
		return createKeySql(list);
	}
	
	/**
	 * 将字段拼接成Key sql
	 * @param list
	 * @return
	 */
	public static String createKeySql(List<String> list){
		
		if(list==null || list.size()<=0){
			return null;
		}
		
		StringBuffer keySql = new StringBuffer();
		keySql.append("primary key (");
		for (int i = 0; i < list.size(); i++) {
			if(i!=0 ){
				keySql.append(",");
			}
			keySql.append(list.get(i));
		}
		keySql.append(")");
		
		return keySql.toString();
	}
	
	
	
	
	
	

	public static PreparedStatement createPreStmt(Connection conn, String sql) {
		PreparedStatement pstmt = null;
		try {
			pstmt = conn.prepareStatement(sql);
		} catch (SQLException e) {
			System.out.println("生成预处理stmt失败");
			e.printStackTrace();
		}
		return pstmt;
	}
	
	public static Statement createStmt(Connection conn) {
		Statement pstmt = null;
		try {
		 	pstmt = conn.createStatement();
		} catch (SQLException e) {
			System.out.println("生成预处理stmt失败");
			e.printStackTrace();
		}
		return pstmt;
	}

	
	
	/**
	 * 解析ResultSet的单条记录,不进行 ResultSet 的next移动处理
	 * 
	 * @param rs
	 * @return
	 */
	private static Map<String, Object> parseResultSetToMap(ResultSet rs) {
		//
		if (null == rs) {
			return null;
		}
		//
		Map<String, Object> map = new HashMap<String, Object>();
		//
		try {
			ResultSetMetaData meta = rs.getMetaData();
			//
			int colNum = meta.getColumnCount();
			//
			for (int i = 1; i <= colNum; i++) {
				// 列名
				String name = meta.getColumnLabel(i); // i+1
				Object value = rs.getObject(i);
				// 加入属性
				map.put(name, value);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		//
		return map;
	}
	

//	/**
//	 * varchar2 number date char clob nchar
//	 * 
//	 * @return
//	 */
//	public static Map createMappingRelation() {
//		Map map = new HashMap();
//		map.put("varchar2", "varchar");
//		map.put("number", "varchar");
//		map.put("date", "datetime");
//		map.put("char", "charo");
//		map.put("clob", "mediumtext");
//		map.put("nchar", "varchar");
//		return map;
//	}

}

 

 

通过dmd 获取字段信息 请参考:http://blog.sina.com.cn/s/blog_707a9f0601014y1y.html

 

 

转载请注明出处:http://314649444.iteye.com/admin/blogs/2307007

 

 

分享到:
评论

相关推荐

    JDBC连接MsSQL/MYSQL/Oracle等多数据库的JSP代码.rar

    本压缩包内含有MSSQL数据库备份文件,以及基于JDBC连接MsSQL/MYSQL/Oracle/Access等常用数据库的示例代码,比较经典的低级问题,笔者也曾遇到过当初学JSp的时候,曾经因为不会连接数据库而很苦恼,这次你可参考...

    JSP_JDBC.rar_oracle

    本压缩包内含有MSSQL数据库备份文件,以及基于JDBC连接MsSQL/MYSQL/Oracle/Access等常用数据库的示例代码,比较经典的低级问题,笔者也曾遇到过当初学JSp的时候,曾经因为不会连接数据库而很苦恼,这次你可参考...

    基于Java Swing + MySQL + JDBC 的图书管理系统.zip

    MySQL 是一款广受欢迎的开源关系型数据库管理系统(RDBMS),由瑞典MySQL AB公司开发,现隶属于美国甲骨文公司(Oracle)。自1998年首次发布以来,MySQL以其卓越的性能、可靠性和可扩展性,成为全球范围内Web应用...

    一项基于Java+Mysql+JDBC的银行中间业务系统项目.zip

    MySQL 是一款广受欢迎的开源关系型数据库管理系统(RDBMS),由瑞典MySQL AB公司开发,现隶属于美国甲骨文公司(Oracle)。自1998年首次发布以来,MySQL以其卓越的性能、可靠性和可扩展性,成为全球范围内Web应用...

    超强MySQL课程笔记_V4.0.ctb

    备份恢复 主备复制 读写分离 HA架构 监控审计 压力测试 性能优化 自动化运维 ==数据的存储方式 1. 人工管理阶段 2. 文件系统阶段 3. 数据库系统管理阶段 ==数据库技术构成 1. 数据库系统 DBS A.数据库管理...

    JDBCdrivers

    自己找了很久都没找到informix的jdbc驱动,最后在一个软件哪里找到了,还是一套的,本来想免费发出来的,但资源分数哪里最小只能是1,所以就当是自己备份了吧,除了db2,其他的都没测试过可不可用,db列表:Db2、...

    Java数据库迁移备份软件源代码.rar

    作者 : 杨浚用于把数据库里的数据导出为insert语句,用于数据备份或迁移到另一个数据库,支持oracle、sybase、mysql和ms sqlServer数据库,大文本和二进制字段被忽略,开发者可以修改参数文件、增加新的jdbc驱动来...

    Jsp 使用JDBC连接各种数据库的实例代码

    本压缩包内含有MSSQL数据库备份文件,以及基于JDBC连接MsSQL/MYSQL/Oracle/Access等常用数据库的示例代码,比较经典的低级问题,笔者也曾遇到过当初学JSp的时候,曾经因为不会连接数据库而很苦恼,这次你可参考...

    把任意数据库导出为其它数据库的insert(可执行程序+java代码)

    当前支持oracle、sybase、mysql和ms sqlServer数据库。 你可以修改参数文件、增加新的jdbc驱动来增加新数据库种类的支持。 使用方法: 软件为免安装的绿色软件。系统必须已经安装1.4版本以上的java虚拟机。 解...

    Navicat Premium 11.0.10 + Patch

    Navicat Premium 是一个可多重连接的数据库管理工具,让你以单一程序同時连接到 MySQL、SQL Server、SQLite、Oracle 和 PostgreSQL 数据库,令管理不同类型的数据库更加方便。 结合了其 Navicat 成员的功能的 ...

    Java数据库备份迁移程序的源代码

    Java完成的程序可以把数据库里的数据导出为其它数据库的insert语句,用于数据备份或迁移到另一个数据库,注意,在数据转换过程中,文本和二进制字段将被忽略,程序支持oracle、sybase、mysql和ms sqlServer数据库。...

    dbproxy]基于阿里cobar增强的opencloudb

    • 在支持Mysql的基础上,后端增加更多的开源数据库和商业数据库的支持,包括原生支持PosteSQL、FireBird等开源数据库,以及通过JDBC等方式间接支持其他非开源的数据库如Oracle、DB2、SQL Server等 • 实现更为智能...

    JSP企业人事管理系统(源代码+论文).rar

    同时,系统使用了JDBC(Java Database Connectivity)技术连接数据库,支持多种关系型数据库,如MySQL、Oracle等,方便企业根据自身需求选择合适的数据库。此外,JSP企业人事管理系统还具备以下特点:界面友好、操作...

    Java开发实战1200例(第1卷).(清华出版.李钟尉.陈丹丹).part3

    书名:《Java开发实战1200例(第I卷)》(清华大学出版社.李钟尉,陈丹丹) PDF格式扫描版,全书分为24章,共817页。...实例269 文件选择对话框指定数据库备份文件 342 实例270 指定打开对话框的文件类型 343 ...

    EasyJForum v3.0 Java技术的社区论坛.rar

     支持 Mysql, Oracle, SQL Server, Hsqldb 等数据库  可视化的编辑器,支持文本、图片、Flash、MP3 等媒体文件的发布和浏览  支持站内短消息、在线收藏夹、论坛统计、全文检索等功能  支持不同界面风格的定制...

    EasyJForum v3.0 简体中文 GBK.zip

    支持 Mysql, Oracle, SQL Server, Hsqldb 等数据库 可视化的编辑器,支持文本、图片、Flash、MP3 等媒体文件的发布和浏览 支持站内短消息、在线收藏夹、论坛统计、全文检索等功能 支持不同界面风格的定制和切换 ...

    EasyJForum v3.0 繁体中文 UTF8.zip

    支持 Mysql, Oracle, SQL Server, Hsqldb 等数据库 可视化的编辑器,支持文本、图片、Flash、MP3 等媒体文件的发布和浏览 支持站内短消息、在线收藏夹、论坛统计、全文检索等功能 支持不同界面风格的定制和切换 ...

Global site tag (gtag.js) - Google Analytics