抛弃框架:基于MVC模式的CRUD和Servlet(最简单的JavaWeb程序)

本文探讨了在小型项目中避免使用复杂框架的情况,分享了一种基于MVC模式的JavaWeb程序实现。通过包结构、数据库配置、CRUD操作以及URL过滤和字符集设置,展示了如何用Servlet进行基础功能开发。文章还提到了设计模式的应用,如单例模式和策略模式,并鼓励读者尝试使用反射机制和注解,以简化配置。

对于一些小项目模块时,很多时候我们使用繁琐的框架实在有些麻烦(导包多还可能版本不兼容,搭建麻烦),当离开框架后我们如何更好的撸代码呢?本文首先感谢我的老师龙sir的授业指导,文中部分代码和思想来源于我的老师,我只是结合自己的编程经验和实际开发对其进行了总结和修改整理,希望同大家分享,如果有不足之处望大家指出,希望大家能够共同学习和进步!

先上图,基于MVC模式的包结构:


然后聊聊增删改查,开始上Util包代码:

首先数据库配置文件的加载——JdbcUtil

1、(.properties文件的加载方式很多,大家可以上论坛搜索下)

2、单例模式此处建议使用饿汉式减少线程冲突,不建议以下模式,设计模式大家可以参考我上传的 “Java常用设计模式源码”

http://download.youkuaiyun.com/detail/zyp689/9828252

package com.zyp168.util;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

/*
 * 数据库访问JDBC工具类
 */
public class JdbcUtil {
	// ---单例模式---

	private static JdbcUtil jdbcUtil;
	private static Properties properties = new Properties();
	private static String jdbc_driver;
	private static String jdbc_url;
	private static String jdbc_user;
	private static String jdbc_pwd;
	//懒汉式-单例模式
	private JdbcUtil() {

	}
	public static JdbcUtil getInstance() {
		if (jdbcUtil == null) {
			jdbcUtil = new JdbcUtil();
		}
		return jdbcUtil;

	}
	// ---静态代码块---
	static {
		InputStream inputStream = Thread.currentThread().getContextClassLoader()
				.getResourceAsStream("mysqlConfig.properties");
		try {
			properties.load(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		}
		jdbc_driver = properties.getProperty("driver");
		jdbc_url = properties.getProperty("url");
		jdbc_user = properties.getProperty("user");
		jdbc_pwd = properties.getProperty("pwd");
		try {
			Class.forName(jdbc_driver);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	// ---获取连接---
	public Connection getConnection() {
		Connection connection = null;
		try {

			connection = DriverManager.getConnection(jdbc_url, jdbc_user, jdbc_pwd);
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return connection;

	}

	// ---关闭资源---
	public void close(Connection connection, Statement stmt, ResultSet rs) {
		if (rs != null) {
			try {
				rs.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		if (stmt != null) {
			try {
				stmt.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		if (connection != null) {
			try {
				connection.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

}

注:单例模式

package com.zyp168.single;

/**
 * 单例模式
 * 
 * 饿汉式的优势在于能够有效解决线程并发问题; static修饰的变量会被作为GC的root根节点不能被回收;static
 * 字段(类的成员变量)在类的所有实例中只存在一次;static会将被修饰者与类直接产生引用关系,而非与类的实例。
 * final修饰的变量值不会改变可节省空间,修饰的类不能被继承。
 * 
 */
public class SingleTest {
	public static void main(String[] args) {
		// 由于private修饰并重写了构造方法,故不能使用new LSingle()创建对象
		LSingle.getInstance();
		ESingle.getInstance();
	}
}

/*
 * 懒汉式
 */
class LSingle {
	private static LSingle lSingle = null;

	private LSingle() {
		//拒绝外部类new LSingle()方式创建对象
	}

	public static LSingle getInstance() {
		if (lSingle == null) {
			lSingle = new LSingle();
		}
		return lSingle;
	}

}

/*
 * 饿汉式
 */
class ESingle {
	// 此处final是为了节省空间,可以不要
	private static final ESingle eSingle = new ESingle();

	private ESingle() {
		//拒绝外部类new ESingle()方式创建对象
	}

	public static ESingle getInstance() {
		return eSingle;
	}
}
注:不同数据库的配置文件格式:(下面是mysql版、 oracle XE版、sqlserver2008 XE版)



其次CRUD模版——JdbcTemplate

package com.zyp168.util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;


/*
 * Jdbc的模板:提取出更新和查询方法封装
 */
public class JdbcTemplate {
	// 单例模式:static修饰的字段可以直接建立引导关系(不需要使用new其类再调用,可以直接用类.方法的形式调用),且只会被创建一次,位于gc根目录不被回收,会造成内存泄露
	private static JdbcTemplate jdbcTemplate;

	private JdbcTemplate() {
		// 默认构造方法
	}

	public static JdbcTemplate getInstance() {
		if (jdbcTemplate == null) {
			jdbcTemplate = new JdbcTemplate();
		}
		return jdbcTemplate;

	}

	// 封装更新操作(增、删、改)
	public boolean update(String sql, Object[] objArr) {
		// 初始化成员变量
		boolean flag = false;
		Connection con = null;
		PreparedStatement pstmt = null;
		// 注册并链接
		con = JdbcUtil.getInstance().getConnection();
		try {
			// pstmt
			pstmt = con.prepareStatement(sql);
		if (objArr!=null) {
			for (int i = 1; i <= objArr.length; i++) {
				pstmt.setObject(i, objArr[i - 1]);
			}	
		}
			// 执行
			int result = pstmt.executeUpdate();
			// 受影响行数大于零返回ture
			flag = (result > 0);
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			JdbcUtil.getInstance().close(con, pstmt, null);

		}
		return flag;

	}

	// 封装单条查询操作
	public Object findOneInfo(String sql, Object[] objArr, JdbcMapper mapper) {
		// 初始化成员变量
		Connection con = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		Object rsObject = null;
		// pstmt
		try {
			con = JdbcUtil.getInstance().getConnection();
			pstmt = con.prepareStatement(sql);
			if (objArr!=null) {
				for (int i = 1; i <= objArr.length; i++) {
					pstmt.setObject(i, objArr[i - 1]);
				}	
			}
			// 执行
			rs = pstmt.executeQuery();
			if (rs.next()) {
				rsObject = mapper.mappinng(rs);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			// 查询
			JdbcUtil.getInstance().close(con, pstmt, rs);
		}
		return rsObject;
	}

	// 封装查询多条操作

	public List<? extends Object> findAllInfo(String sql, Object[] objArr,
			JdbcMapper mapper) {
		List<Object> list = new ArrayList<Object>();
		// 初始化变量
		Object rsObject = null;
		// Connection.createStatement() 创建一个 Statement 对象来将 SQL 语句发送到数据库。
		Connection con = null;
		// PreparedStatement表示预编译的 SQL 语句的对象,Statement 对象的子接口
		PreparedStatement pstmt = null;
		// ResultSet查询获得的数据表,next方法将光标移动到下一行对象(对应数据库表中的行),没有下一行时返回 false;
		ResultSet rs = null;
		try {
			// 注册并链接
			con = JdbcUtil.getInstance().getConnection();
			// 创建PreparedStatement对象
			pstmt = con.prepareStatement(sql);
			if (objArr!=null) {
				for (int i = 1; i <= objArr.length; i++) {
					pstmt.setObject(i, objArr[i - 1]);
				}	
			}
			// 执行
			rs = pstmt.executeQuery();
			while (rs.next()) {
				rsObject = mapper.mappinng(rs);
				list.add(rsObject);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			JdbcUtil.getInstance().close(con, pstmt, rs);
		}
		return list;

	}
}

然后ORM映射文件——JdbcMapper (想下Hibernate或Mabatis中的xml映射文件是否感觉特别熟悉?大笑

package com.zyp168.util;

import java.sql.ResultSet;

/*
 * 对象映射接口
 */
public interface JdbcMapper {
	public abstract Object mappinng(ResultSet rs);
}

实例:

package com.zyp168.model.mapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.zyp168.model.User;
import com.zyp168.util.JdbcMapper;

public class UserMapper implements JdbcMapper {

	@Override
	public Object mappinng(ResultSet rs) {
		User user = new User();
		try {
			//ORM映射 
			user.setId(rs.getInt("id"));
			user.setName(rs.getString("name"));
			user.setPwd(rs.getString("pwd"));
		} catch (SQLException e) {
			// 实体类属性和数据库表中字段的映射关系不匹配
			e.printStackTrace();
		}
		return user;
	}

}

实例:原生的CRUD \ 第三方DbUtil包\ JdbcTemplate比较

package com.zyp168.dao.impl;

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

import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import com.zyp168.dao.UserDao;
import com.zyp168.model.User;
import com.zyp168.util.JdbcTemplate;
import com.zyp168.util.JdbcUtil;
import com.zyp168.util.PageModel;

public class UserDaoImpl implements UserDao {
	/*
	 * 单例模式--饿汉式
	 */
	private final static UserDaoImpl userDaoImpl=new UserDaoImpl();
	private UserDaoImpl() {
	}
	public static UserDao getInstance() {
		return userDaoImpl;
	}
	@Override
	public boolean register(User user) {
		/**
		 * TODO 原生Jdbc实现更新操作
		 */
		boolean flag = false;
		Connection connection = null;
		PreparedStatement pstmt = null;
		// 注册驱动,连接数据库
		try {
			Class.forName("com.mysql.jdbc.Driver");
			connection = DriverManager.getConnection(
					"jdbc:mysql://localhost:3306/test", "root", "root");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		// 编写Sql
		String sql = "INSERT INTO user VALUES(NULL,?,?)";
		// 预编译Sql
		try {
			pstmt = connection.prepareStatement(sql);
			pstmt.setObject(1, user.getName());
			pstmt.setObject(2, user.getPwd());
		} catch (SQLException e) {
			e.printStackTrace();
		}
		// 执行Sql并返回结果
		try {
			int result = pstmt.executeUpdate();// 受影响条数
			flag = result > 0;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		// 关闭资源
		JdbcUtil.getInstance().close(connection, pstmt, null);
		return flag;
	}

	@Override
	public User login(User user) {
		/**
		 * TODO 原生Jdbc实现查询操作
		 */
		User model = new User();
		Statement stmt = null;
		ResultSet rs = null;
		Connection connection = JdbcUtil.getInstance().getConnection();
		StringBuffer sbf = new StringBuffer();
		sbf.append("SELECT * FROM user WHERE name='").append(user.getName())
				.append("' AND pwd='").append(user.getPwd() + "'");
		String sql = sbf.toString();
		// 编译执行Sql并返回结果
		try {
			stmt = connection.createStatement();
			// 返回的是第一行的各属性的Map键值对(键分类存储),其中next()方法将游标移动到下一行位置,如果下一行为空返回false
			rs = stmt.executeQuery(sql);
			if (rs.next()) {
				model.setId(rs.getInt("id"));
				model.setName(rs.getString("name"));
				model.setPwd(rs.getString("pwd"));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		JdbcUtil.getInstance().close(connection, stmt, rs);
		return model;
	}

	@Override
	public boolean repwd(User user) {
		/**
		 * TODO 使用自定义封装的JdbcTemplate+JdbcMapper模板进行操作
		 */
		String sql = "UPDATE user SET pwd=? WHERE name=?";
		Object[] objArr = { user.getPwd(), user.getName() };
		return JdbcTemplate.getInstance().update(sql, objArr);
	}

	@Override
	public List<User> findByCondition(Map<String, String> condition,
			PageModel<User> page) {
		/**
		 * TODO 使用第三方工具包DbUtils实现,API地址如下
		 * http://commons.apache.org/proper/commons-dbutils/apidocs/
		 */
		List<User> list = new ArrayList<User>();
		Connection connection = JdbcUtil.getInstance().getConnection();
		StringBuffer sb = new StringBuffer();
		/* 查询条件集合 */
		List<Object> objects = new ArrayList<Object>();
		// 查询所有
		if (condition == null) {
			sb.append("SELECT * FROM user WHERE 1 = 1 ");
		}
		// 模糊查询
		if (condition != null) {
			if (condition.get("name") != null || condition.get("name") != "") {
				sb.append(" AND name LIKE CONCAT('%',?,'%')");
				objects.add(condition.get("name"));
			}
		}
		// 分页查询
		if (page != null) {
			sb.append("LIMIT ?, ?");
			objects.add((page.getPageNum() - 1) * page.getPageSize());
			objects.add(page.getPageSize());
		}
		//
		QueryRunner queryRunner = new QueryRunner();
		try {
			list = queryRunner.query(connection, sb.toString(),
					new BeanListHandler<User>(User.class), objects);
		} catch (SQLException e) {
			e.printStackTrace();
		}
		DbUtils.closeQuietly(connection);
		return list;
	}
}

简单说说分页实例(不完整,按实际情况灵活处理)——PageModel、PageTag

package com.zyp168.util;

import java.io.Serializable;
import java.util.List;

/**
 * 分页要求:根据页码获取该页的数据集合(注:每页数据行数大小已知) 
 * 分页实体类: pageNum 页码/当前页 pageSize 每页大小 pageCount 总页数 recordCount 总记录数   List<T> 分页查询获得的数据集合 
 * 主要方法:首页 上一页 下一页 尾页 查询所有获取总记录数 分页查询获取当前页码数据集合
 */
public class PageModel<T> implements Serializable {
	// Java序列化
	private static final long serialVersionUID = 1L;
	// 分页相关参数
	private Integer pageNum;
	private Integer pageSize; //常量
	private Integer recordCount; //BaseDaoImpl.findAll()查询
	private Integer pageCount; //可根据上述三参数计算获得: 改写 PageModel<T>.getPageCount() 方法
	private List<T> pageList; //BaseDaoImpl<T>.queryByPage(PageModel page)查询
	
	// 自定义成员方法 :首页 上一页 下一页 尾页  
	/**
	 * 获取首野
	 * @return 1
	 */
	public Integer getFirstPage() {
		return 1;
	}
	/**
	 * 获取上一页
	 * @return
	 */
	public Integer getPreviousPage() {
		if (pageNum <= 1) {
			return 1;
		} else {
			return pageNum - 1;
		}
	}
	/**
	 * 获取下一页
	 * @return
	 */
	public Integer getNextPage() {
		if (pageNum >= this.getPageCount()) {
			return this.getPageCount();
		} else {
			return pageNum + 1;
		}
	}
	/**
	 * 获取末页
	 * @return
	 */
	public Integer getLastPage() {
		return this.getPageCount();
	}
	
	// 构造函数
	public PageModel() {
		super();
	}
	public PageModel(Integer pageNum, Integer pageSize, Integer recordCount,
			Integer pageCount, List<T> pageList) {
		super();
		this.pageNum = pageNum;
		this.pageSize = pageSize;
		this.recordCount = recordCount;
		this.pageCount = pageCount;
		this.pageList = pageList;
	}
	// getter 和 setter方法 
	public Integer getPageNum() {
		return pageNum;
	}
	public void setPageNum(Integer pageNum) {
		this.pageNum = pageNum;
	}
	public Integer getPageSize() {
		return pageSize;
	}
	public void setPageSize(Integer pageSize) {
		this.pageSize = pageSize;
	}
	public Integer getRecordCount() {
		return recordCount;
	}
	public void setRecordCount(Integer recordCount) {
		this.recordCount = recordCount;
	}
	/**
	 * 改写默认getPageCount方法
	 * @return
	 */
	public Integer getPageCount() {
		//方法一:
		if (recordCount % pageSize == 0) {
			pageCount= recordCount / pageSize;
		} else {
			pageCount= recordCount / pageSize + 1;
		}
		return pageCount;
		//方法二:
		//	return	pageCount = (recordCount + pageSize - 1) / pageSize; 
		
	}
	public void setPageCount(Integer pageCount) {
		this.pageCount = pageCount;
	}
	public List<T> getPageList() {
		return pageList;
	}
	public void setPageList(List<T> pageList) {
		this.pageList = pageList;
	}
	public static long getSerialversionuid() {
		return serialVersionUID;
	}
}

package com.zyp168.util;

import java.io.IOException;

import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.SimpleTagSupport;

/**
 * 分页标签:拼写要输出到页面的HTML文本 请求地址 url 分页相关参数 page
 */
public class PageTag<T> extends SimpleTagSupport {
	// 此处可以继承TagSupport或SimpleTagSupport接口(注意:Java单继承,只能继承一个)

	private String url; // 请求URI
	private PageModel<T> page; // 分页相关参数

	public void doTag() {
		JspWriter out = getJspContext().getOut();// SimpleTagSupport接口
		// PrintWriter out = response.getWriter();

		StringBuffer buffer = new StringBuffer();
		// StringBuilder sbl = new StringBuilder();

		/**
		 * //拼写要输出到页面的HTML文本
		 */
		// 拼接“第一页”和“上一页”
		if (page.getPageNum()==1) {
			buffer.append("第一页  上一页");
		}else {
			buffer.append("<a href='");
			buffer.append(url);
			buffer.append("&pageNum=1'>第一页</a>");
			buffer.append("<a href='");
			buffer.append(url);
			buffer.append("&pageNum=");
			buffer.append(page.getPageNum()-1);
			buffer.append("'>上一页</a>");
		}
		// 循环拼接页码
		for (int i = 1; i <= page.getPageCount(); i++) {
			buffer.append("<a href='");
			buffer.append(url);
			buffer.append("&pageNum=");
			buffer.append(i);
			buffer.append("'>");
			buffer.append(i);
			buffer.append("</a>");
		}
		// 拼接“下一页”和“最后一页”
		if (page.getPageNum()>=page.getPageCount()) {
			buffer.append("下一页  最后一页");
		}else {
			buffer.append("<a href='");
			buffer.append(url);
			buffer.append("&pageNum=");
			buffer.append(page.getPageNum()+1);
			buffer.append("&'>下一页</a>");
			buffer.append("<a href='");
			buffer.append(url);
			buffer.append("&pageNum=");
			buffer.append(page.getPageCount());
			buffer.append("'>最后一页</a>");
		}
		// 拼接共*页
		buffer.append("  共");
		buffer.append(page.getPageCount());
		buffer.append("页");
		// 向页面输出分页的内容
		try {
			out.print(buffer.toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	//
	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public PageModel<T> getPage() {
		return page;
	}

	public void setPage(PageModel<T> page) {
		this.page = page;
	}

}

Web中servlet部分实例:

(1)建议大家将所有jsp页面都放入WEB-INFO下,编写URL过滤器类,并将密码采用MD5加密保证安全,此处不详叙。

(2)大家可以将字符集部分做成过滤器,没必要每次设置字符集(想下spring的字符过滤配置)

(3)建议大家使用注解方式,减少web.xml配置(想下SpringMVC的注解)

(4)截取url地址交给不同方法处理的策略模式,是否让大家想起struct 和 springmvc的风格?

(5)Java反射机制学的比较好的同志们,不如尝试下反向生成对象?(想下Spring的依赖注入和控制反转)

package com.zyp168.web.servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.zyp168.model.User;
import com.zyp168.service.UserService;
import com.zyp168.service.impl.UserServiceImpl;

@WebServlet(name = "userServlet", urlPatterns = { "/user/*" })
public class UserServlet extends HttpServlet {
	// 成员变量
	private static final long serialVersionUID = 1L;
	private UserService userService = new UserServiceImpl();

	/**
	 * Servlet中main方法:service() doGet() doPost()等
	 * service()方法没有用static修饰,可以直接调用类内的非静态方法,尽量少使用static;
	 */
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 字符集配置(建议放入字符集过滤器进行设置)
		request.setCharacterEncoding("utf-8");
		response.setContentType("text/html;charset=utf-8");
		// 解析请求信息URL
		String uri = request.getRequestURI();// 获取的地址为"/SOSO/user/execute.action"
		String action = uri.substring(uri.lastIndexOf("/") + 1, uri.lastIndexOf("."));
		// 匹配逻辑处理方法
		if ("execute".equals(action)) {
			execute(request, response);
		}
		if ("register".equals(action)) {
			register(request, response);
		}
		if ("login".equals(action)) {
			// 第一域名相同的单点登陆SSO(Single Sign On)
			login(request, response);
		}
		if ("repwd".equals(action)) {
			repwd(request, response);
		}
		if ("findByCondition".equals(action)) {
			findByCondition(request, response);
		}
	}

	// 逻辑处理方法
	private void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		/**
		 * 访问WEB-INF的入口方法
		 */
		request.getRequestDispatcher("/WEB-INF/jsp/" + request.getParameter("uri")).forward(request, response);
	}

	private String register(HttpServletRequest request, HttpServletResponse response) {

		return null;

	}

	private void login(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		String name = request.getParameter("name");
		String password = request.getParameter("password");
		User model = new User();
		model.setName(name);
		model.setPwd(password);
		if (name != null && password != null) {
			User user = userService.login(model);
			if (model != null) {
				if (name.equals(user.getName()) && password.equals(user.getPwd())) {
					HttpSession session = request.getSession();
					session.setAttribute("user", user);
					// SSO相关操作:写入cookie
					Cookie cookie = new Cookie("sso", user.getName());
					cookie.setMaxAge(3600);// 一小时
					cookie.setDomain(".zyp168.com");// www.bbs.zyp168.com 和
													// www.news.zyp168.com
					cookie.setPath("/"); // 如果为/demo_01表示仅仅demo_01项目可以获得
					response.addCookie(cookie);
					response.sendRedirect("/WEB-INF/jsp/index.jsp");
				}
			}
		}

		String msg = "您输入的帐户或密码有误,请重新登陆!";
		request.setAttribute("msg", msg);
		request.getRequestDispatcher("/redirect.jsp").forward(request, response);

	}

	private String repwd(HttpServletRequest request, HttpServletResponse response) {

		return null;

	}

	private String findByCondition(HttpServletRequest request, HttpServletResponse response) {

		return null;

	}
}

本文先写到这里,不足之处希望大家指出,文中实例代码仅供参考,希望大家重思想,不要纠结代码,本文有待完善,希望大家提供宝贵意见,谢谢!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值