• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

jilongliang/kotlin: Kotlin +SpringBoot + MyBatis完美搭建最简洁最酷的前后端分离框 ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

jilongliang/kotlin

开源软件地址(OpenSource Url):

https://github.com/jilongliang/kotlin

开源编程语言(OpenSource Language):

Kotlin 97.8%

开源软件介绍(OpenSource Introduction):

一、为什么学习(初衷):

  • 1、经过调研很多培训机构大量投入课程的产出,说明在新一代的编程领域有一定的地位,它前程应用一定会有更好的广泛的使用。
  • 2、阿里p3c扫描代码ReView插件底层大量使用了Kotlin开发与使用,
  • 3、据说SpringBoot2.以上的全家桶很多组件底层框架和Spring5.x版本用到kotlin支持开发(拥抱Kotlin)。
  • 4、在2017年在朋友圈偶遇它的影子。
  • 5、2018年准备学习kotlin相关的热身工作,在学习过程中,把牺牲个人周末休息时间,尝试把公司的Spring Cloud工程改写成Kotlin框架。
  • 6、当然了现在学习和了解它,不一定现在就要用它,主要目的是为以后做准备。
  • 7、作为技术人员,希望为kotlin的生态作出一份渺小的贡献。

二、技术选型

Kotlin框架.png

  • 解析器:JSOUP、FastJSON
  • 开发工具:JDK1.8 、Maven 、Eclipse
  • 技术框架:SpringBoot
  • ORM技术:MyBatisPlus
  • 数据库:MySQL
  • Apache 工具:HttpClient、Lang3
  • Git代码版本控制

三、kotlin背景简要描述

  • Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,被称之为 Android 世界的Swift,由 JetBrains 设计开发并开源。Kotlin 可以编译成Java字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。
  • 在Google I/O 2017中,Google 宣布 Kotlin 成为 Android 官方开发语言。
  • Kotlin支持表达式语法编程非常友好,以简洁代码等众多方面强大功能,吸引很多开发者的青睐。

四、Spring Boot 发展路线简要描述

  • 随着动态语言的流行 (Ruby、Groovy、Scala、Node.js),Java 的开发显得格外的笨重:繁多的配置、低下的开发效率、复杂的部署流程以及第三方技术集成难度大。
  • 在上述环境下,Spring Boot 应运而生。它使用“习惯优于配置”(项目中存在大量的配置,此外还内置了一个习惯性的配置,让你无需手动进行配置)的理念让你的项目快速的运行起来。使用 Spring Boot 很容易创建一个独立运行(运行 Jar,内嵌 Servlet 容器)准生产级别的基于 Spring 框架的项目,使用 Spring Boot 你可以不用或者只需很少的 Spring 配置。

4.1 SpringBoot插件使用

  • spring-boot-starter-actuator actuator是监控系统健康情况的工具
  • spring-boot-devtools 实现热部署,实际开发过程中,修改应用的业务逻辑时常常需要重启应用,这显得非常繁琐,降低了开发效率,所以热部署对于开发来说显得十分必要了
  • spring-boot-starter-aop 此插件没什么好说的了,aop是spring的两大功能模块之一,功能非常强大,为解耦提供了非常优秀的解决方案。如:面向方面编程
  • spring-boot-starter-tomcat spring boot 内置Tomcat插件
  • spring-boot-starter-test 测试工具
  • mybatis-spring-boot-starter spring boot整合MyBatis的jar
  • spring-boot-maven-plugin Spring Boot Maven plugin能够将Spring Boot应用打包为可执行的jar或war文件,然后以通常的方式运行Spring Boot应用。

五、Kotlin插件

  • kotlin-stdlib-jdk8 这个是kotlin的标准jar,也就是最基础的一个工程,底层封装了大量kotlin的语法和实现调用逻辑,也是最复杂的一个工程‘引入工程会出现这三个jar工程 kotlin-stdlib-jdk8-1.2.20.jar kotlin-stdlib-1.2.20.jar kotlin-stdlib-jdk7-1.2.20.jar

  • kotlin-reflect reflect 顾名思义就是反射工程了

六、jsoup简要

  • jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
  • 个人认为jsoup是最好的解析器,在很多场景都能见到他的影子,不单只可以解析HTML所有结构,还可以解析XML,在做爬虫器最为广泛。

七、fastJson

八、HttpClient

  • HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本。

九、Maven

十、MyBatis-Plus

十一、工程准备

  • 方法一、打开Eclipse Marketplace-->>输入kotlin在线安装即可 image.png
  • 方法二、下载离线安装包(不提供教程,说明有这种方式) image.png
  • 方法三、可以使用优秀的kotlin编辑器eg: IntelliJ IDEA

十二、工程结构

image.png image.png image.png

十三、工程代码结构

  • SpringBoot入口类
@EnableAsync
@Configuration
@EnableScheduling
@EnableAutoConfiguration  //启用读取配置
@ComponentScan("com.flong.kotlin")  //扫描com.flong文件目录下
@SpringBootApplication(scanBasePackages = ["com.flong.kotlin"] )
//@SpringBootApplication(scanBasePackages = arrayOf("com.flong.kotlin")) 这种写法也OK
open class Application {
   @Bean
   open fun jspViewResolver(): InternalResourceViewResolver {
   	var resolver = InternalResourceViewResolver();
   	resolver.setPrefix("/WEB-INF/views/");
   	resolver.setSuffix(".jsp");
   	return resolver;
   }
   //静态类
   companion object {

   	/**启动SpringBoot的主入口.*/
   	@JvmStatic fun main(args: Array<String>) {
   		//*args的星号表示引用相同类型的变量
   		SpringApplication.run(Application::class.java, *args)
   	}
   }
}

  • WebCofig工具类统一处理配置

  • 消息转换器,中文乱码,Long的精度长度问题,时间格式等问题
  • cors 跨域支持 可以用@CrossOrigin在controller上单独设置
  • 统一处理请求URL拦截器
@Configuration
@ConditionalOnClass(WebMvcConfigurer::class)
@Order(Ordered.HIGHEST_PRECEDENCE)
open class WebConfig : WebMvcConfigurer{

	constructor() : super()

	@Bean
	open fun customConverters(): HttpMessageConverters {
		//创建fastJson消息转换器
		var fastJsonConverter = FastJsonHttpMessageConverter()
		//创建配置类
		var fastJsonConfig = FastJsonConfig()
		//修改配置返回内容的过滤
		fastJsonConfig.setSerializerFeatures(
				// 格式化
				SerializerFeature.PrettyFormat,
				// 可解决long精度丢失 但会有带来相应的中文问题
				//SerializerFeature.BrowserCompatible,
				// 消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)
				SerializerFeature.DisableCircularReferenceDetect,
				// 是否输出值为null的字段,默认为false
				SerializerFeature.WriteMapNullValue,
				// 字符类型字段如果为null,输出为"",而非null
				SerializerFeature.WriteNullStringAsEmpty,
				// List字段如果为null,输出为[],而非null
				SerializerFeature.WriteNullListAsEmpty
		)
		// 日期格式
		fastJsonConfig.dateFormat = "yyyy-MM-dd HH:mm:ss"
		
		// long精度问题
		var serializeConfig = SerializeConfig.globalInstance
		serializeConfig.put(Integer::class.java, ToStringSerializer.instance)
		serializeConfig.put(BigInteger::class.java, ToStringSerializer.instance)
		serializeConfig.put(Long::class.java, ToStringSerializer.instance)
		serializeConfig.put(Long::class.javaObjectType, ToStringSerializer.instance)
		fastJsonConfig.setSerializeConfig(serializeConfig)
		
		//处理中文乱码问题
		var fastMediaTypes = ArrayList<MediaType>()
		fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8)
		fastMediaTypes.add(MediaType(MediaType.TEXT_HTML, Charset.forName("UTF-8")))
		fastMediaTypes.add(MediaType(MediaType.TEXT_PLAIN, Charset.forName("UTF-8")))
		fastMediaTypes.add(MediaType(MediaType.APPLICATION_FORM_URLENCODED, Charset.forName("UTF-8")))
		fastMediaTypes.add(MediaType.MULTIPART_FORM_DATA)
		
		fastJsonConverter.setSupportedMediaTypes(fastMediaTypes)
		fastJsonConverter.setFastJsonConfig(fastJsonConfig)
		//将fastjson添加到视图消息转换器列表内
		return HttpMessageConverters(fastJsonConverter)
	}

	/**
	 * 拦截器
	 */
	open override fun addInterceptors(registry: InterceptorRegistry) {
		//registry.addInterceptor(logInterceptor).addPathPatterns("/**")
		//registry.addInterceptor(apiInterceptor).addPathPatterns("/**")
	}

	/**
	 * cors 跨域支持 可以用@CrossOrigin在controller上单独设置
	 */
	open override fun addCorsMappings(registry: CorsRegistry) {
		registry.addMapping("/**")
				//设置允许跨域请求的域名
				.allowedOrigins("*")
				//设置允许的方法
				.allowedMethods("*")
				//设置允许的头信息
				.allowedHeaders("*")
				//是否允许证书 不再默认开启
				.allowCredentials(java.lang.Boolean.TRUE)
	}
}
  • BaseServcie的业务逻辑基类的封装
open class BaseService<M : BaseMapper<T>, T, Q : Query> : ServiceImpl<M, T> {
	//构造方法
	constructor() : super()
	var IN_SIZE: Int = 1000;
	//新增
	fun add(obj: T): Boolean {
		var affCnt = baseMapper.insert(obj);
		return null != affCnt && affCnt > 0;
	}
       //更加id去更新数据
	override fun updateById(obj: T): Boolean {
		var affCnt = baseMapper.updateById(obj);
		return null != affCnt && affCnt > 0;
	}
	/**
	 * 删除
	 */
	override fun deleteById(id: Serializable): Boolean {
		var affCnt = baseMapper.deleteById(id);
		return null != affCnt && affCnt > 0;
	}
	/**
	 * ID 取对象,取不到为空
	 */
	fun get(id: Serializable): T {
		return baseMapper.selectById(id);
	}
	fun getMapper(): M {
		return this.baseMapper;
	}
	fun buildQuery(query: Q): EntityWrapper<T> {
		return EntityWrapper<T>();
	}
}
@Configuration
@MapperScan(basePackages = arrayOf(DataSourceConfig.PACKAGE), sqlSessionFactoryRef = "sessionFactory")
open class DataSourceConfig  {
	//静态常量
	companion object {
		//const 关键字用来修饰常量,且只能修饰  val,不能修饰var,  companion object 的名字可以省略,可以使用 Companion来指代
         const val  PACKAGE = "com.flong.kotlin.*.mapper";
		 const val TYPEALIASESPACKAGE = "com.flong.kotlin.modules.entity";
    }

	final var MAPPERLOCATIONS = "classpath*:mapper/*.xml";
	@Primary
	@Bean("dataSource")
	@ConfigurationProperties(prefix = "spring.datasource")
	open fun dataSource(): DataSource {
		return DruidDataSource();
	}


	@Bean
	open fun transactionManager(@Qualifier("dataSource") dataSource: DataSource): DataSourceTransactionManager {
		return DataSourceTransactionManager(dataSource);
	}

	@Bean
	open fun sessionFactory(dataSource: DataSource):SqlSessionFactory {
		//===1.mybatis-plus globalConfig配置
	    var cfg =  GlobalConfiguration();
		
	    // 字段的驼峰下划线转换
	    cfg.setDbColumnUnderline(true);
	    // 全局主键策略
	    cfg.setIdType(IdType.AUTO.key);
	    // 全局逻辑删除
		cfg.sqlInjector 		= LogicSqlInjector()
		cfg.logicDeleteValue 	= "1"
		cfg.logicNotDeleteValue = "0"
	
	    //===2.构造sessionFactory(mybatis-plus)
	    var sf = MybatisSqlSessionFactoryBean();
	    sf.setDataSource(dataSource);
	    sf.setMapperLocations(PathMatchingResourcePatternResolver().getResources(MAPPERLOCATIONS));
	    sf.setGlobalConfig(cfg);
		sf.setTypeAliasesPackage(TYPEALIASESPACKAGE) 
	    // 分页插件
		sf.setPlugins(arrayOf(PaginationInterceptor()))
	    //请注意:这种return sf.getObject();与return sf.`object`写法完全一样,但是object是kotiln的关键字,所以要用 【单引号】引起来
		return sf.`object`
	}

	
	
	/**
	 * @Description 初始化druid
	 * @Author		liangjl
	 * @Date		2018年1月17日 下午4:52:05
	 * @return 参数
	 * @return ServletRegistrationBean 返回类型 
	 */
	@Bean
	open fun druidServlet() : ServletRegistrationBean<Servlet>{
		var bean:ServletRegistrationBean<Servlet> = ServletRegistrationBean(StatViewServlet(), "/druid/*") ;
		/** 初始化参数配置,initParams**/
	    //登录查看信息的账号密码.
	    bean.addInitParameter("loginUsername", "root");
	    bean.addInitParameter("loginPassword", "root");
	    //IP白名单 (没有配置或者为空,则允许所有访问)
	    bean.addInitParameter("allow", "");
	    //IP黑名单 (共存时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
	    bean.addInitParameter("deny", "192.88.88.88");
	    //禁用HTML页面上的“Reset All”功能
	    bean.addInitParameter("resetEnable", "false");
		return bean;
	}
	@Bean
	open fun filterRegistrationBean() : FilterRegistrationBean<Filter>{
		var filterRegistrationBean =   FilterRegistrationBean<Filter>()
		filterRegistrationBean.setFilter(WebStatFilter());
		filterRegistrationBean.addUrlPatterns("/*");
		filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
		return filterRegistrationBean;
	}

}
  • 分页工具类PageVO

open class PageVO<E>  {
	/**
	 * 总条数
	 */
	var total: Long? = null
	/**
	 * 查询结果
	 */
	var records: Collection<E>? = null
	/**
	 * 当前页
	 */
	var page: Int? = null
	/**
	 * 总页数
	 */
	var totalPage: Long? = null
	/**
	 * 每页显示条数
	 */
	var pageSize: Int? = null
	//init { }

	//多重构造方法
	constructor(records: Collection<E>?, pageVO: PageVO<E>){
		this.records = records;
		this.total = pageVO.total;
		this.totalPage = pageVO.totalPage;
		this.page = pageVO.page;
		this.pageSize = pageVO.pageSize;
		
	}
	//多重构造方法
	constructor(records: Collection<E>?, page: Page<Any>){
		this.records = records;
		this.total = page.getTotal();
		this.totalPage = page.getPages();
		this.page = page.getCurrent();
		this.pageSize = page.getSize();
		
	}
}
  • 分页工具类PageUtil

open class PageUtil {
  /**
   * 取mybatis-plus分页对象
   */
  open fun getPage(query : Query):Page<Any>? {
      return Page(query.page, query.pageSize);
  }
}
  • 表名与字段的映射的JavaBean实体类

/**
 *data保持数据data class就是一个类中只包含一些数据字段,类似于vo,pojo,java bean。一般而言,
 *我们在Java中定义了这个数据类之后要重写一下toString,equals等方法。要生成get,set方法
 *https://www.cnblogs.com/liuliqianxiao/p/7152773.html
 *注意mybatis查询数据,然后封装实体的时候,构造方法这里有点儿坑,查询的字段必须与构造方法一直。
 */
@TableName("t_user")
data  class User constructor(
	@TableId(value = "user_id", type = IdType.ID_WORKER)
	var userId: Long?= null,//用户Id主键,IdWork生成
	var userName: String? = null,//用户名
	var passWord: String? = null,//密码
	@TableLogic
	@JSONField(name = "isDeleted")
	var isDeleted: Int? = null,//删除
	var createTime: Date? = null //创建时间,允许为空,让数据库自动生成即可
  ) :Serializable{
 
	//手动重写toString方法
	override fun toString(): String {
		return "[User(userId = $userId,userName = $userName, passWord=$passWord,isDeleted=$isDeleted,createTime=$createTime),]"
	}

	//equals
	override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as User
        if (userId != other.userId) return false
        if (userName != other.userName) return false
		if (passWord != other.passWord) return false
		if (isDeleted != other.isDeleted) return false
		if (createTime != other.createTime) return false
        return true
    }
	override fun hashCode(): Int {
        var result = userId?.hashCode() ?: 0
        result = 31 * result + (userName?.hashCode() ?: 0)
        result = 31 * result + (passWord?.hashCode() ?: 0)
        result = 31 * result + (isDeleted?.hashCode() ?: 0)
        result = 31 * result + (createTime?.hashCode() ?: 0)
        
        return result
    }

}
  • MyBatis的Mapper接口

interface IUserMapper : BaseMapper<User>{

	//这里的?表示当前是否对象可以为空 
    //@see http://blog.csdn.net/android_gjw/article/details/78436707
	fun getUserList(query : UserQuery , page : Page<Any>): List<UserRespVo>?
 
}
  • IUserMapper对应的MyBatis的XMl

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.flong.kotlin.modules.mapper.IUserMapper">
	<!-- 抽取公共字段 -->
	<sql id="find_AS_R">
	    <![CDATA[
	    	SELECT A.user_Id as userId, A.USER_NAME as userName, 
	    	A.PASS_WORD as passWord,is_deleted as isDeleted,
	    	A.create_time as createTime
	    ]]>
	</sql>

	<!-- 用户查询 -->
	<select id="getUserList" resultType="com.flong.kotlin.modules.vo.resp.UserRespVo" >
		<include refid="find_AS_R" /> FROM T_USER A 
		<where>
			<if test="userId != null">	and A.user_Id = #{userId}</if>
			<if test="userName != null and  userName !='' ">and A.USER_NAME = #{userName}</if>
		</where>
	</select>
</mapper>

  • 业务Service代码ORM

  • 对象关系映射(英语:(Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。
  • 此类是用于与MyBatis的Mapper进行映射进行操作ORM操作,在这里不过多的详解ORM,在代码中看到操作几乎都是MyBatisPlus自身封装的CRUD的操作,除了分页是自定义MyBatis的XML进行处理。这里提供这样的支持,主要是为了处理对业务复杂拓展查询场景。
@Service
open class UserService : BaseService<IUserMapper,User,UserQuery>() {
	//Kotlin lateinit 和 by lazy 的区别
	//http://blog.csdn.net/Sherlbon/article/details/72769843
	@Autowired lateinit var userMapper: IUserMapper;
	//根据
	open fun queryAllUser(): List<User>? {
		var wrapper = createWrapper();
		return this.selectList(wrapper);
	}
	open fun listPage(query : UserQuery) : PageVO<UserRespVo> ? {
		var page = PageUtil().getPage(query)  ;// 设置分页
		var dataList = userMapper.getUserList(query, page!!);//page!!强制告诉编辑器不可能为空
	    
		var json = JSON.toJSONString(dataList);
		println(json)
		return  PageVO(dataList, page);// 获取分页数和总条数
	}
		
       //通过主键id进行查询
	open fun getUserId(userId: Long): User? {
		return get(userId);
	}

	//插入用户
	open fun addUser() {
		var userId = IdWorker.getId();
		var u = User(userId, "liangjl", "123456",null, Date());
		var json = JSON.toJSONString(u);
		println(json)
		add(u);
	}
	fun createWrapper(): Wrapper<User> {
		var wrapper = EntityWrapper<User>();
		wrapper.setEntity(User());//设置实体
		return wrapper;
	}

}
  • Controller代码的CRUD的代码实现

  • C - Create 对数据添加或增加操作
  • R - Retrieve 对数据读取查询操作
  • U - Update 对数据更新或修改操作
  • D - Delete 对数据删除操作
@RestController
@RequestMapping("/rest")
open class UserController : BaseController(){
	
    @Autowired private lateinit var userService: UserService
 
  @RequestMapping("/list1")
    fun list1():  String{
		return "NewFile" //跳转页面
    }
	
    //添加
   @RequestMapping("/add")
    fun add():  Unit{
		userService.addUser()
    }
   //根据用户Id进行删除用户信息
   @RequestMapping("/deletedById")
    fun deletedById(userId : Long):  Unit{
		userService.deleteById(userId);
    }

   //更新用户信息,通过Id唯一主键进行操作。
   @RequestMapping("/update")
    fun update(user : User):  Unit{
		userService.updateById(user)
    }
	
	//根据Id查询用户
	@RequestMapping("/getUserId")
    fun getUserId(userId :Long):Any?{
		var user = userService.getUserId(userId);
		if(user ==null){
			var msgCode = UserMsgCode.FIND_NOT_USER;
			throw BaseException(msgCode.code!! ,msgCode.message!!)
		}
		return userService.getUserId(userId)
    }
	
   //查询所有用户信息
    @RequestMapping("/queryAllUser")
    fun queryAllUser():List<User>?{
	   return userService.queryAllUser()
    }
	
    //分页查询
    @RequestMapping("listPage")
    fun listPage(query :UserQuery) :PageVO<UserRespVo> ? {
    	   var listPage = userService.listPage(query);
           return listPage;
    }
	
    @RequestMapping("/getBody")
    fun getBody(@RequestBody jsonText:UserRespVo){
	   println(jsonText.component2())
	   println(jsonText.userName)
    }
}
  • profiles数据库配置

logging.evel.root=info
logging.evel.com.flong.kotlin=debug

# =======mybatis_config=====
    
spring.profiles.active=dev
#=======datasource========
#spring.datasource.name=na
spring.datasource.url=jdbc:mysql://localhost:3306/kotlin?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#spring.datasource.filters=stat,wall,log4j,slf4j
##配置支持【emoji表情】与【不合法输入参数】
##作用:用于处理请求与响应一些不可信的字符串
spring.datasource.connectionInitSqls=set names utf8mb4
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

spring.devtools.restart.enabled=false


spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
 

  • Apache HttpClient的网络Http通信


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap