为了账号安全,请及时绑定邮箱和手机立即绑定

Spring之路(34)–使用xml配置Spring+SpringMVC+MyBatis(SSM)项目完整实例

标签:
Spring

概述

本篇介绍下如何使用xml配置SSM项目(如果要表达的更加严谨,其实除了xml配置,还有注解配置),并实现对单表(还是之前一直举例的blog表)增删改查操作。

因为需要配置的东西比较多,所以还是要简单的说下思路,先理顺了整体思路,才容易理解每个局部是在干什么。

  1. 首先需要新建一个网站项目,然后引入相关的jar包,因为SSM框架是别人封装好的,所以需要引入别人的jar。
  2. 因为是网站项目,所以需要配置web.xml,这个配置文件是网站项目配置文件。当网站启动时,会加载其中的配置。
  3. 需要配置spring-config.xml,我需要对spring容器进行配置,包括控制器组件、服务组件、数据库操作、数据源组件、MyBatis组件,都需要容器进行管理,spring配置是整个项目的核心配置。从此处可以看出MyBatis是通过spring集成进来的,在Spring一切皆组件的思想下,集成一个第三方框架是一个很简单事情。
  4. 配置mybatis-config.xml,此处是对mybatis进行配置,mybatis组件在进入容器管理时,携带该配置信息进入。spring负责管理mybatis组件,mybatis组件初始化时需要的配置信息就在mybatis-config.xml
  5. 依次开发数据库操作层、服务层、控制器层,前端视图页面,然后进行操作验证。

通过项目结构图了解下整体项目结构跟构建顺序:

在这里插入图片描述

1 新建项目

File-New-Other Project-Dynamic Web Project,建立一个动态网站项目,项目名称xmlssmdemo

2 导入jar包

除了spring相关的jar包(已经指示过多次不再具体指示),还需要导入下面的jar包:

  1. commons-logging-1.2.jar 日志相关
  2. jackson-annotations-2.8.0.jar json相关
  3. jackson-core-2.8.0.jar json相关
  4. jackson-databind-2.8.0.jar json相关
  5. mysql-connector-java-5.1.48.jar mysql驱动
  6. druid-1.1.21.jar 数据库连接池
  7. mybatis-3.5.3.jar mybatis相关
  8. mybatis-spring-2.0.3.jar mybatis-spring相关

3 配置web.xml

当我们的网站项目启动时,会加载web.xml配置,此时我们要配置下DispatcherServlet及其匹配的映射路径,同时引入spring容器的配置文件spring-config.xml,具体如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	id="WebApp_ID" version="3.1">
	<display-name>xmlssmdemo</display-name>
	<servlet>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<!-- 引入spring-config.xml配置文件,该文件用来配置spring容器 -->
			<param-value>/WEB-INF/spring-config.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<!-- 配置DispatcherServlet对应所有请求(*表示所有) -->
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

4 配置spring-config.xml

很多开发者喜欢将spring配置保存到几个不同的xml配置文件中,本项目中由于我们的项目比较简单,所以直接放到一个配置文件中就好了,具体配置内容和配置顺序如下,请注意注释:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
         http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
         http://www.springframework.org/schema/context 
         http://www.springframework.org/schema/context/spring-context-4.0.xsd
         http://www.springframework.org/schema/mvc
         http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
	<!-- 第一步,配置MyBatis用来操作数据,sqlSessionFactory就是MyBatis中用来操作数据库的 -->
	<bean id="sqlSessionFactory"
		class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<!-- 引入Mybatis的配置文件,MyBatis组件需要它 -->
		<property name="configLocation"
			value="classpath:org/maoge/xmlssmdemo/config/mybatis-config.xml" />
	</bean>
	<!-- 第二步,配置数据源,可以看到sqlSessionFactory需要注入该数据源 -->
	<bean id="dataSource"
		class="com.alibaba.druid.pool.DruidDataSource">
		<property name="driverClassName"
			value="com.mysql.jdbc.Driver"></property>
		<property name="url"
			value="jdbc:mysql://127.0.0.1:3306/myblog?useUnicode=true&amp;characterEncoding=utf-8"></property>
		<property name="username" value="root"></property>
		<property name="password" value="Easy@0122"></property>
	</bean>
	<!-- 第三步,将org.maoge.xmlssmdemo.dao包下的类注册为bean,注意此处是注册为MyBatis方式管理的可操作数据库的bean -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="sqlSessionFactoryBeanName"
			value="sqlSessionFactory" />
		<property name="basePackage" value="org.maoge.xmlssmdemo.dao" />
	</bean>
	<!-- 第四步,扫描控制器层和服务层 -->
	<context:component-scan
		base-package="org.maoge.xmlssmdemo.controller" />
	<context:component-scan
		base-package="org.maoge.xmlssmdemo.service" />
	<!--第五步,开启通过注解配置访问路径与方法的匹配 -->
	<mvc:annotation-driven />
	<!--第六步,配置静态资源映射 -->
	<mvc:resources mapping="/static/**" location="/static/" />
</beans>

简单的总结下,就是引入MyBaits组件,然后MyBatis是访问数据库的操作组件,还需要数据源,所以配置上数据源组件。在配置MyBatis组件时,还需要指定MyBatis组件的配置文件。

然后就是要扫描控制器层、服务层、数据库操作层,其中数据库操作层因为使用了MyBatis,所以需要使用一个定义好的MapperScannerConfigurer类型的bean来扫描该层。

最后就是常用配置了,开了注解驱动,然后配置了对静态资源访问的映射。

5、配置mybatis-config.xml

在配置spring.xml时,有如下一行代码:

   <bean id="sqlSessionFactory"
   	class="org.mybatis.spring.SqlSessionFactoryBean">
   	<property name="dataSource" ref="dataSource" />
   	<!-- 引入Mybatis的配置文件,MyBatis组件需要它 -->
   	<property name="configLocation"
   		value="classpath:org/maoge/xmlssmdemo/config/mybatis-config.xml" />

可以看出,Spring容器中有sqlSessionFactory这个bean组件,而该组件有个参数是mybatis-config.xml,也就是说Spring容器在注册sqlSessionFactory时,会加载mybatis-config.xml文件信息。

该xml文件制定了ORM元数据文件的位置,ORM元数据文件就是保存数据库表与Java对象之间映射关系的。mybatis-config.xml代码如下:

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE configuration  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<mappers><!-- 映射器告诉MyBatis到哪里去找映射文件 -->
		<mapper resource="org/maoge/xmlssmdemo/config/BlogMapper.xml" />
	</mappers>
</configuration> 

6、编写映射文件

再捋下子加载流程哈,别懵了。web项目启动–加载web.xml–加载配置的spring.xml–加载MyBatis的sqlSessionFactory组件–加载MyBatis配置文件mybatis-config.xml读取其中的映射信息。

所谓的映射,这个就是告诉我们的程序,数据库中的表blog对应Java类BlogDo,然后也得将表中的列与BlogDo中的属性对应关系描述出来。这样如果是由10行数据的表格,就对应size为10的List<BlogDo>,这个必须得理解啊,如果这个理解不了,那…,还是先补补基础知识吧。

OK,下面我们来具体看下到底是怎么映射的哈,BlogMapper.xml如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.maoge.xmlssmdemo.dao.BlogDao">
	<select id="getById" parameterType="Long"
		resultType="org.maoge.xmlssmdemo.xdo.BlogDo">
		select * from blog where id = #{id}
	</select>
</mapper>

首先下面这部分,是固定的头部,不用去管。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

然后,看下面部分,namespace的值跟BlogDao类有一个对应,也就是说当执行BlogDao类中的方法时,会从该配置文件中找对应的配置来执行。

<mapper namespace="org.maoge.xmlssmdemo.dao.BlogDao">

然后,下面部分表示,当执行BlogDao类中的getById方法时,会转成下面对应的sql语句来执行,即执行select * from blog where id = #{id},同时#{id}是一个占位符号,根据parameterType="Long",会把BlogDao类中的getById方法的Long类型的参数,注入到select * from blog where id = #{id}#{id}的位置,也就是说如果执行getById(101),则会转换成执行sqlselect * from blog where id = 101
执行结果根据resultType="org.maoge.xmlssmdemo.xdo.BlogDo",也就是说列名会自动对应到BlogDo同名的属性中。

	<select id="getById" parameterType="Long"
		resultType="org.maoge.xmlssmdemo.xdo.BlogDo">
		select * from blog where id = #{id}
	</select>

所以通过映射文件,程序真正知道了当执行数据操作层(Dao)层方法时,该如何转换为执行的sql语句,又该如何将结果返回为Java对象。

7、开发各层逻辑代码

剩下的工作就都是类似的了,无非就是记忆下增删改查具体在BlogMapper.xml文件中的写法,此处我们贴下代码:

BlogMapper.xml代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.maoge.xmlssmdemo.dao.BlogDao">
	<!-- 获取一个 -->
	<select id="getById" parameterType="Long"
		resultType="org.maoge.xmlssmdemo.xdo.BlogDo">
		select * from blog where id = #{id}
	</select>
	<!-- 获取列表 -->
	<select id="getList"
		resultType="org.maoge.xmlssmdemo.xdo.BlogDo">
		select * from blog
	</select>
	<!-- 插入 -->
	<insert id="insert"
		parameterType="org.maoge.xmlssmdemo.xdo.BlogDo">
		insert into
		blog(author,content,title)values(#{author},#{content},#{title})
	</insert>
	<!-- 更新 -->
	<update id="update"
		parameterType="org.maoge.xmlssmdemo.xdo.BlogDo">
		update blog set
		author=#{author},content=#{content},title=#{title} where
		id=#{id}
	</update>
	<!-- 删除 -->
	<delete id="delete" parameterType="Long">
		delete from blog where
		id=#{id}
	</delete>
</mapper>

然后BlogDao里面的方法名字与BlogMapper.xml中的id一一对应:

package org.maoge.xmlssmdemo.dao;

import java.util.List;

import org.maoge.xmlssmdemo.xdo.BlogDo;
import org.springframework.stereotype.Repository;

@Repository
public interface BlogDao {
   BlogDo getById(Long id);

   List<BlogDo> getList();

   int insert(BlogDo blog);

   int update(BlogDo blog);

   int delete(Long id);
}

剩下的数据对象BlogDo、服务类BlogService、控制类BlogController与视图页面blog.html与之前一模一样了,直接贴代码:

package org.maoge.xmlssmdemo.xdo;
/**
* @theme 数据对象--博客
* @author maoge
* @date 2020-01-29
*/
public class BlogDo {
   private Long id;
   private String title;
   private String author;
   private String content;
   // 省略get get
}
package org.maoge.xmlssmdemo.service;

import java.util.List;

import org.maoge.xmlssmdemo.dao.BlogDao;
import org.maoge.xmlssmdemo.xdo.BlogDo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BlogService {
	@Autowired
	private BlogDao blogDao;

	/**
	 * 获取博客列表
	 */
	public List<BlogDo> getBlogList() {
		return blogDao.getList();
	}

	/**
	 * 按id获取博客信息
	 */
	public BlogDo getBlogById(Long id) {
		return blogDao.getById(id);
	}

	/**
	 * 新增博客
	 */
	public void addBlog(BlogDo blog) {
		blogDao.insert(blog);
	}

	/**
	 * 根据博客id更新博客信息
	 */
	public void updateBlog(BlogDo blog) {
		blogDao.update(blog);
	}

	/**
	 * 根据博客id删除对应博客
	 */
	public void deleteBlog(Long id) {
		blogDao.delete(id);
	}
}
package org.maoge.xmlssmdemo.controller;

import java.util.List;

import org.maoge.xmlssmdemo.service.BlogService;
import org.maoge.xmlssmdemo.xdo.BlogDo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @theme 控制器--博客
 * @author maoge
 * @date 2020-01-28
 */
@RestController // 通过该注解,第一将BlogController注册为控制器,第二将其中方法返回值转换为json
public class BlogController {
	@Autowired // 自动装配blogService
	private BlogService blogService;

	/**
	 * 查询博客信息 1、@GetMapping表示可以使用get方法请求该api
	 * 2、"/blog/{id}"表示请求路径为/blog/{id}的形式,其中{id}为占位符
	 * 3、@PathVariable("id")表示将占位符{id}的值传递给id 4、也就是说/blog/123请求的话,会将123传递给参数id
	 */
	@GetMapping(value = "/blog/{id}")
	public BlogDo getOne(@PathVariable("id") long id) {
		return blogService.getBlogById(id);
	}

	/**
	 * 查询博客列表,使用get方法
	 */
	@GetMapping("/blog")
	public List<BlogDo> getList() {
		return blogService.getBlogList();
	}

	/**
	 * 新增博客 1、@PostMapping表示使用post方法
	 * 2、@RequestBody表示将请求中的json信息转换为BlogDo类型的对象信息,该转换也是由SpringMVC自动完成的
	 */
	@PostMapping("/blog")
	public void add(@RequestBody BlogDo blog) {
		blogService.addBlog(blog);
	}

	/**
	 * 修改博客 实际上此处也可以不在路径中传递id,而是整个使用json传递对象信息,但是我查询了一些文档,貌似使用路径传递id更加规范一些,此处不用纠结
	 */
	@PutMapping("/blog/{id}")
	public void update(@PathVariable("id") long id, @RequestBody BlogDo blog) {
		// 修改指定id的博客信息
		blog.setId(id);
		blogService.updateBlog(blog);
	}

	/**
	 * 删除博客
	 */
	@DeleteMapping("/blog/{id}")
	public void delete(@PathVariable("id") long id) {
		blogService.deleteBlog(id);
	}
}
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title></title>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet"
	href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
	integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
	crossorigin="anonymous">
</head>

<body>
	<nav class="navbar navbar-inverse">
		<div class="container-fluid">
			<ul class="nav navbar-nav">
				<li><a href="#" onclick="viewBlogs()">浏览博客</a></li>
				<li><a href="#" onclick="addBlog()">新增博客</a></li>
			</ul>
		</div>
	</nav>
	<table id="blogTable" class="table table-striped">
		<tr>
			<th>ID</th>
			<th>标题</th>
			<th>作者</th>
			<th>操作</th>
		</tr>
	</table>
	<!-- 新增弹窗 -->
	<div id="blogAddModal" class="modal fade" tabindex="-1" role="dialog">
		<div class="modal-dialog" role="document">
			<div class="modal-content">
				<div class="modal-header">
					<h4 class="modal-title">新增博客</h4>
				</div>
				<div class="modal-body" style="padding: 16px;">
					<!-- 新增博客的表单 -->
					<form>
						<div class="form-group">
							<label>标题</label> <input name="title" type="text"
								class="form-control">
						</div>
						<div class="form-group">
							<label>内容</label>
							<textarea name="content" class="form-control" rows="3"></textarea>
						</div>
						<div class="form-group">
							<label>作者</label> <input name="author" type="text"
								class="form-control">
						</div>
					</form>
				</div>
				<div class="modal-footer">
					<button type="button" class="btn btn-primary"
						onclick="addBlogSubmit()">提交</button>
					<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
				</div>
			</div>
		</div>
	</div>
	<!-- 编辑弹窗 -->
	<div id="blogEditModal" class="modal fade" tabindex="-1" role="dialog">
		<div class="modal-dialog" role="document">
			<div class="modal-content">
				<div class="modal-header">
					<h4 class="modal-title">编辑博客</h4>
				</div>
				<div class="modal-body" style="padding: 16px;">
					<!-- 编辑博客的表单 -->
					<form>
						<div class="form-group">
							<label>ID</label> <input name="id" type="text"
								class="form-control" readonly>
						</div>
						<div class="form-group">
							<label>标题</label> <input name="title" type="text"
								class="form-control">
						</div>
						<div class="form-group">
							<label>内容</label>
							<textarea name="content" class="form-control" rows="3"></textarea>
						</div>
						<div class="form-group">
							<label>作者</label> <input name="author" type="text"
								class="form-control">
						</div>
					</form>
				</div>
				<div class="modal-footer">
					<button type="button" class="btn btn-primary"
						onclick="editBlogSubmit()">提交</button>
					<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
				</div>
			</div>
		</div>
	</div>
</body>
<!--jQuery-->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script
	src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
	integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
	crossorigin="anonymous">
	
</script>
<script>
	//浏览博客
	function viewBlogs() {
		var row = "";
		//先清空表格
		$('#blogTable').find("tr:gt(0)").remove();
		$
				.ajax({
					type : "GET",
					url : "/xmlssmdemo/blog",
					dataType : "json",
					contentType : "application/json; charset=utf-8",
					success : function(res) {
						$
								.each(
										res,
										function(i, v) {
											row = "<tr>";
											row += "<td>" + v.id + "</td>";
											row += "<td>" + v.title + "</td>";
											row += "<td>" + v.author + "</td>";
											row += "<td><a class='btn btn-primary btn-sm' href='#' onclick='editBlog("
													+ v.id + ")'>编辑</a>";
											row += "<a class='btn btn-danger btn-sm' href='#' onclick='deleteBlog("
													+ v.id + ")'>删除</a></td>";
											row += "</tr>";
											$("#blogTable").append(row);
										});
					},
					error : function(err) {
						console.log(err);
					}
				});
	}
	//新增
	function addBlog() {
		$('#blogAddModal').modal('show');
	}
	//新增提交
	function addBlogSubmit() {
		var data = {
			id : '',
			title : $("#blogAddModal input[name='title']").val(),
			author : $("#blogAddModal input[name='author']").val(),
			content : $("#blogAddModal textarea[name='content']").val()
		};
		$.ajax({
			type : "POST",
			url : "/xmlssmdemo/blog",
			//dataType: "json",
			contentType : "application/json; charset=utf-8",
			data : JSON.stringify(data), //需要将对象转换为字符串提交
			success : function() {
				//新增后重新加载
				viewBlogs();
				//关闭弹窗
				$('#blogAddModal').modal('hide');
			},
			error : function(err) {
				console.log(err);
			}
		});
	}
	//编辑
	function editBlog(id) {
		//查询博客信息
		$.ajax({
			type : "GET",
			url : "/xmlssmdemo/blog/" + id,
			dataType : "json",
			contentType : "application/json; charset=utf-8",
			success : function(res) {
				console.log(res);
				//为编辑框赋值
				$("#blogEditModal input[name='id']").val(res.id);
				$("#blogEditModal input[name='title']").val(res.title);
				$("#blogEditModal input[name='author']").val(res.author);
				$("#blogEditModal textarea[name='content']").val(res.content);
				//显示编辑弹窗
				$('#blogEditModal').modal('show');
			},
			error : function(err) {
				console.log(err);
			}
		});
	}
	//编辑提交
	function editBlogSubmit() {
		var data = {
			id : $("#blogEditModal input[name='id']").val(),
			title : $("#blogEditModal input[name='title']").val(),
			author : $("#blogEditModal input[name='author']").val(),
			content : $("#blogEditModal textarea[name='content']").val()
		};
		$.ajax({
			type : "PUT",
			url : "/xmlssmdemo/blog/" + data.id,
			//dataType: "json",
			contentType : "application/json; charset=utf-8",
			data : JSON.stringify(data), //需要将对象转换为字符串提交
			success : function() {
				//新增后重新加载
				viewBlogs();
				//关闭弹窗
				$('#blogEditModal').modal('hide');
			},
			error : function(err) {
				console.log(err);
			}
		});
	}
	//删除
	function deleteBlog(id) {
		$.ajax({
			type : "DELETE",
			url : "/xmlssmdemo/blog/" + id,
			//dataType: "json",//由于删除方法无返回值,所以此处注释掉
			contentType : "application/json; charset=utf-8",
			success : function() {
				//删除后重新加载
				viewBlogs();
			},
			error : function(err) {
				console.log(err);
			}
		});
	}
</script>

</html>

总结

看明白了,基于Spring组件思想之后,我们只是把使用SpringJDBC实现的BlogDao替换为了使用MyBatis实现的BlogDao,其他的完全不需要变化。

同时之前SpringJDBC需要的数据源dataSource,与现在MyBatis使用的数据源dataSource组件配置完全一样。

Spring将功能、模块、类库等都化为了组件,召之即来挥之即去,非常类似于工业标准件的概念,大大提高了生产效率。

而为何我们广泛的采用了MyBatis、SpringJDBC等标准件,而不是自行开发的组件呢,就是因为大厂出品,经过千锤百炼的验证了,成本低效率高。

OVER。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
软件工程师
手记
粉丝
1.5万
获赞与收藏
1523

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消