基于AndroidPN搭建Android的推送平台 - 离线消息的推送

一. 原理图

  1. 没实现离线消息推送功能前,项目的架构图如下

  2. 实现离线消息推送功能后,项目的架构图如下 - 版本一 - 适用于新闻等app工作环境,对离线消息的到达率要求不高的环境

  3. 实现离线消息推送功能后,项目的架构图如下 - 版本二 - 适用于IM等app工作环境,对离线消息的到达率要求99.99%

二. 版本一的实现

  1. 创建数据表 - Notification

    a. 创建实体类

package org.androidpn.server.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "notification")
public class Notification {
	
	@Id
     @GeneratedValue(strategy = GenerationType.AUTO)
	private long id;
	
	@Column(name = "api_key", length = 64)
	private String apiKey;
	
	@Column(name = "username", nullable = false, length = 64)
	private String username;
	
	@Column(name = "title", nullable = false, length = 64)
	private String title;
	
	@Column(name = "message", nullable = false, length = 1024)
	private String message;
	
	@Column(name = "uri", length = 256)
	private String uri;
	
	public Notification() {
		
	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getApiKey() {
		return apiKey;
	}

	public void setApiKey(String apiKey) {
		this.apiKey = apiKey;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public String getUri() {
		return uri;
	}

	public void setUri(String uri) {
		this.uri = uri;
	}

}

    b. 修改Hibernate的配置文件hibernate.cfg.xml

<!-- Mapping Files -->
<mapping class="org.androidpn.server.model.User" />
<!-- 消息映射 -->
<mapping class="org.androidpn.server.model.Notification" />

  2. Dao层封装

    a. 新建一个NotificationDao接口及它的实现NotificationDaoHibernate.java

package org.androidpn.server.dao;

import java.util.List;

import org.androidpn.server.model.Notification;

public interface NotificationDao {
	
	void saveNotification(Notification notification);
	
	List<Notification> findNotificationsByUsername(String username);
	
	void deleteNotification(Notification notification);
}

  

package org.androidpn.server.dao.hibernate;

import java.util.List;

import org.androidpn.server.dao.NotificationDao;
import org.androidpn.server.model.Notification;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class NotificationDaoHibernate extends HibernateDaoSupport implements
		NotificationDao {

	public void saveNotification(Notification notification) {
		getHibernateTemplate().saveOrUpdate(notification);
		getHibernateTemplate().flush();
	}

	public void deleteNotification(Notification notification) {
		// TODO Auto-generated method stub
		getHibernateTemplate().delete(notification);
	}

	@SuppressWarnings("unchecked")
	public List<Notification> findNotificationsByUsername(String username) {
		// TODO Auto-generated method stub
		List<Notification> list = getHibernateTemplate().find("from Notification where username=?", username);
		if(list != null && list.size()>0) {
			return list;
		}
		return null;
	}

}

    b. 修改Spring的配置文件-spring-config.xml

<!-- =============================================================== -->
<!-- Data Access Objects -->
<!-- =============================================================== -->

<bean id="userDao" class="org.androidpn.server.dao.hibernate.UserDaoHibernate">
	<property name="sessionFactory" ref="sessionFactory" />
</bean>
	
<!-- 消息 -->
<bean id="notificationDao" class="org.androidpn.server.dao.hibernate.NotificationDaoHibernate">
	<property name="sessionFactory" ref="sessionFactory" />
</bean>

  3. Service层封装

    a. 新建一个NotificationService接口及它的实现NotificationServiceImpl.java

package org.androidpn.server.service;

import java.util.List;

import org.androidpn.server.model.Notification;

public interface NotificationService {
	void saveNotification(Notification notification);

	List<Notification> findNotificationsByUsername(String username);

	void deleteNotification(Notification notification);
}

  

package org.androidpn.server.service.impl;

import java.util.List;

import org.androidpn.server.dao.NotificationDao;
import org.androidpn.server.model.Notification;
import org.androidpn.server.service.NotificationService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class NotificationServiceImpl implements NotificationService {
	
	protected final Log log = LogFactory.getLog(getClass());
	
	private NotificationDao notificationDao;
	
	public NotificationDao getNotificationDao() {
		return notificationDao;
	}

	public void setNotificationDao(NotificationDao notificationDao) {
		this.notificationDao = notificationDao;
	}

	public void saveNotification(Notification notification) {
		// TODO Auto-generated method stub
		notificationDao.saveNotification(notification);
	}

	public List<Notification> findNotificationsByUsername(String username) {
		// TODO Auto-generated method stub
		return notificationDao.findNotificationsByUsername(username);
	}

	public void deleteNotification(Notification notification) {
		// TODO Auto-generated method stub
		notificationDao.deleteNotification(notification);
	}

}

    b. 修改Spring的配置文件-spring-config.xml

<!-- =============================================================== -->
<!-- Services -->
<!-- =============================================================== -->

<bean id="userService" class="org.androidpn.server.service.impl.UserServiceImpl">
	<property name="userDao" ref="userDao" />
</bean>
	
<bean id="notificationService" class="org.androidpn.server.service.impl.NotificationServiceImpl">
	<property name="notificationDao" ref="notificationDao" />
</bean>

    c. 修改ServiceLocator.java,提供对外调用的应用

package org.androidpn.server.service;

import org.androidpn.server.xmpp.XmppServer;

/** 
 * This is a helper class to look up service objects.
 *
 * @author Sehwan Noh (devnoh@gmail.com)
 */
public class ServiceLocator {

    public static String USER_SERVICE = "userService";
    
    public static String NOTIFICATION_SERVICE = "notificationService";

    /**
     * Generic method to obtain a service object for a given name. 
     * 
     * @param name the service bean name
     * @return
     */
    public static Object getService(String name) {
        return XmppServer.getInstance().getBean(name);
    }

    /**
     * Obtains the user service.
     * 
     * @return the user service
     */
    public static UserService getUserService() {
        return (UserService) XmppServer.getInstance().getBean(USER_SERVICE);
    }
    
    /**
     * Obtains the notification service.
     * 
     * @return the notification service
     */
    public static NotificationService getNotificationService() {
        return (NotificationService) XmppServer.getInstance().getBean(NOTIFICATION_SERVICE);
    }

}

  4. 业务逻辑层实现

    a. 修改NotificationManager.java,添加一个存储消息Notification的方法

/**
* 存储推送消息
* @param apiKey
* @param username
* @param title
* @param message
* @param uri
*/
private void saveNotification(String apiKey, String username, String title,
			String message, String uri) {
	Notification notification = new Notification();
	notification.setApiKey(apiKey);
	notification.setUri(uri);
	notification.setUsername(username);
	notification.setTitle(title);
	notification.setMessage(message);
	// ServiceLocator.getNotificationService().saveNotification(notification);
		notificationService.saveNotification(notification);
}

    b. 修改发送推送消息逻辑

public void sendBroadcast(String apiKey, String title, String message,
			String uri) {
	log.debug("sendBroadcast()...");
	IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
		
	// 通过遍历数据库的用户,发送推送消息
	List<User> allUser = userService.getUsers();
	for(User user : allUser) {
			ClientSession session = sessionManager.getSession(user.getUsername());
		if(session != null && session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		} else {
			saveNotification(apiKey, user.getUsername(), title, message, uri);
		}
	}
		
	// 仅仅遍历在线用户
//	for (ClientSession session : sessionManager.getSessions()) {
//		if (session.getPresence().isAvailable()) {
//			notificationIQ.setTo(session.getAddress());
//			session.deliver(notificationIQ);
//		}
//	}
}

public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri) {
	log.debug("sendNotifcationToUser()...");
	IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		} 
		// 如果用户在线但不可用时,则保存推送消息到数据库中
		else {
			saveNotification(apiKey, username, title, message, uri);
		}
	} 
	// 如果用户不在线但不可用时,则保存推送消息到数据库中
	else {
		User user;
		try {
			// 通过用户名发送推送消息,在保存消息时,验证该用户名是否存储,防止存储无用数据
			user = userService.getUserByUsername(username);
			if (user != null) {
				saveNotification(apiKey, username, title, message, uri);
			}
		} catch (UserNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

    c. 用户重新上线后,向其推送之前的存储在数据库的离线消息 - 修改PresenceUpdateHandler.java中的process()方法

public void process(Packet packet) {
	ClientSession session = sessionManager.getSession(packet.getFrom());

	try {
		Presence presence = (Presence) packet;
		Presence.Type type = presence.getType();

		if (type == null) { // null == available
			if (session != null
						&& session.getStatus() == Session.STATUS_CLOSED) {
				log.warn("Rejected available presence: " + presence + " - "
							+ session);
				return;
			}

			// 用户重新上线
			if (session != null) {
				session.setPresence(presence);
				if (!session.isInitialized()) {
					// initSession(session);
					session.setInitialized(true);
				}

				// 遍历消息数据库
				List<Notification> list = notificationService
							.findNotificationsByUsername(session.getUsername());
				if (list != null && list.size() > 0) {
					for(Notification notification : list) {
						String apiKey = notification.getApiKey();
						String title = notification.getTitle();
						String message = notification.getMessage();
						String uri = notification.getUri();
							notificationManager.sendNotifcationToUser(apiKey, session.getUsername(), title, message, uri);
						// 发送后将该消息从数据库表中删除
							notificationService.deleteNotification(notification);
					}
				}

			}

		} else if (Presence.Type.unavailable == type) {

			if (session != null) {
				session.setPresence(presence);
			}

		} else {
			presence = presence.createCopy();
			if (session != null) {
				presence.setFrom(new JID(null, session.getServerName(),
							null, true));
				presence.setTo(session.getAddress());
			} else {
				JID sender = presence.getFrom();
				presence.setFrom(presence.getTo());
				presence.setTo(sender);
			}
			presence.setError(PacketError.Condition.bad_request);
			PacketDeliverer.deliver(presence);
		}

	} catch (Exception e) {
		log.error("Internal server error. Triggered by packet: " + packet,
					e);
	}
}

三. 版本二的实现 - 版本一的增强版

  1. 为Notification添加一个UUID字段(回执的消息体就是该消息的uuid)

    @Column(name = "uuid", length = 64, nullable = false, unique = true)

    private String uuid;

    // setter - getter

  2. 为Dao及其实现层和Server层及其实现层添加一个根据uuid删除消息的方法

    void deleteNotificationByUUID(String uuid); -> 实现

@SuppressWarnings("unchecked")
public void deleteNotificationByUUID(String uuid) {
	// TODO Auto-generated method stub
	List<Notification> list = getHibernateTemplate().find(
				"from Notification where uuid=?", uuid);
	if (list != null && list.size() > 0) {
		Notification notification = list.get(0);
		deleteNotification(notification);
	}
}

  3. 修改相关业务逻辑

    a. 修改NotificationManager相关方法

package org.androidpn.server.xmpp.push;

import java.util.List;
import java.util.Random;

import org.androidpn.server.model.Notification;
import org.androidpn.server.model.User;
import org.androidpn.server.service.NotificationService;
import org.androidpn.server.service.ServiceLocator;
import org.androidpn.server.service.UserNotFoundException;
import org.androidpn.server.service.UserService;
import org.androidpn.server.xmpp.session.ClientSession;
import org.androidpn.server.xmpp.session.SessionManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.xmpp.packet.IQ;

/**
 * This class is to manage sending the notifcations to the users.
 *
 * @author Sehwan Noh (devnoh@gmail.com)
 */
public class NotificationManager {

	private static final String NOTIFICATION_NAMESPACE = "androidpn:iq:notification";

	private final Log log = LogFactory.getLog(getClass());

	private SessionManager sessionManager;

	private NotificationService notificationService;

	private UserService userService;

	/**
	 * Constructor.
	 */
	public NotificationManager() {
		sessionManager = SessionManager.getInstance();
		notificationService = ServiceLocator.getNotificationService();
		userService = ServiceLocator.getUserService();
	}

	/**
	 * Broadcasts a newly created notification message to all connected users.
	 * 
	 * @param apiKey
	 *            the API key
	 * @param title
	 *            the title
	 * @param message
	 *            the message details
	 * @param uri
	 *            the uri
	 */
	public void sendBroadcast(String apiKey, String title, String message,
			String uri) {
		log.debug("sendBroadcast()...");
		// IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
		// uri);
		// 通过遍历数据库的用户,发送推送消息
		List<User> allUser = userService.getUsers();
		for (User user : allUser) {
			Random random = new Random();
			String id = Integer.toHexString(random.nextInt());
			IQ notificationIQ = createNotificationIQ(id, apiKey, title,
					message, uri);
			ClientSession session = sessionManager.getSession(user
					.getUsername());
			if (session != null && session.getPresence().isAvailable()) {
				notificationIQ.setTo(session.getAddress());
				session.deliver(notificationIQ);
			} /* else { */
			saveNotification(id, apiKey, user.getUsername(), title, message,
					uri);
			// }
		}

		// 仅仅遍历在线用户
		// for (ClientSession session : sessionManager.getSessions()) {
		// if (session.getPresence().isAvailable()) {
		// notificationIQ.setTo(session.getAddress());
		// session.deliver(notificationIQ);
		// }
		// }
	}

	/**
	 * Sends a newly created notification message to the specific user.
	 * 
	 * @param apiKey
	 *            the API key
	 * @param title
	 *            the title
	 * @param message
	 *            the message details
	 * @param uri
	 *            the uri
	 */
	public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri) {
		log.debug("sendNotifcationToUser()...");
		Random random = new Random();
		String id = Integer.toHexString(random.nextInt());
		IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
				uri);
		ClientSession session = sessionManager.getSession(username);
		if (session != null) {
			if (session.getPresence().isAvailable()) {
				notificationIQ.setTo(session.getAddress());
				session.deliver(notificationIQ);
			}
			// // 如果用户在线但不可用时,则保存推送消息到数据库中
			// else {
			// saveNotification(id, apiKey, username, title, message, uri);
			// }
		}
		// // 如果用户不在线但不可用时,则保存推送消息到数据库中
		// else {
		User user;
		try {
			// 通过用户名发送推送消息,在保存消息时,验证该用户名是否存储,防止存储无用数据
			user = userService.getUserByUsername(username);
			if (user != null) {
				saveNotification(id, apiKey, username, title, message, uri);
			}
		} catch (UserNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// }
	}

	/**
	 * 存储推送消息
	 * 
	 * @param apiKey
	 * @param username
	 * @param title
	 * @param message
	 * @param uri
	 */
	private void saveNotification(String uuid, String apiKey, String username,
			String title, String message, String uri) {
		Notification notification = new Notification();
		notification.setUuid(uuid);
		notification.setApiKey(apiKey);
		notification.setUri(uri);
		notification.setUsername(username);
		notification.setTitle(title);
		notification.setMessage(message);
		// ServiceLocator.getNotificationService().saveNotification(notification);
		notificationService.saveNotification(notification);
	}

	/**
	 * Creates a new notification IQ and returns it.
	 */
	private IQ createNotificationIQ(String id, String apiKey, String title,
			String message, String uri) {
		// String id = String.valueOf(System.currentTimeMillis());
		/**
		 * uuid生成策略
		 */
		// Random random = new Random();
		// String id = Integer.toHexString(random.nextInt());
		Element notification = DocumentHelper.createElement(QName.get(
				"notification", NOTIFICATION_NAMESPACE));
		notification.addElement("id").setText(id);
		notification.addElement("apiKey").setText(apiKey);
		notification.addElement("title").setText(title);
		notification.addElement("message").setText(message);
		notification.addElement("uri").setText(uri);

		IQ iq = new IQ();
		iq.setType(IQ.Type.set);
		iq.setChildElement(notification);

		return iq;
	}

}

    b. 客户端向服务器发送消息回执

      1. 新建一个消息回执IQ -> DeliverConfirmIQ.java

package org.androidpn.client;

import org.jivesoftware.smack.packet.IQ;

public class DeliverConfirmIQ extends IQ {
	private String uuid;

	@Override
	public String getChildElementXML() {
		// TODO Auto-generated method stub
		StringBuilder buf = new StringBuilder();
		buf.append("<").append("deliverconfirm").append(" xmlns=\"")
				.append("androidpn:iq:deliverconfirm").append("\">");
		if (uuid != null) {
			buf.append("<uuid>").append(uuid).append("</uuid>");
		}
		buf.append("</").append("deliverconfirm").append("> ");
		return buf.toString();
	}

	public String getUuid() {
		return uuid;
	}

	public void setUuid(String uuid) {
		this.uuid = uuid;
	}

}

      2. 在客户端收到推送消息对外发送广播后,向服务器发送回执消息,修改NotificationPacketListener的processPacket()方法

@Override
public void processPacket(Packet packet) {
    Log.d(LOGTAG, "NotificationPacketListener.processPacket()...");
    Log.d(LOGTAG, "packet.toXML()=" + packet.toXML());

    if (packet instanceof NotificationIQ) {
        	// ...
            xmppManager.getContext().sendBroadcast(intent);
            
            // 向服务器发送消息回执
            DeliverConfirmIQ deliverConfirmIQ = new DeliverConfirmIQ();
            deliverConfirmIQ.setUuid(notificationId);
            deliverConfirmIQ.setType(IQ.Type.SET);
            xmppManager.getConnection().sendPacket(deliverConfirmIQ);
        }
    }

}

    c. 服务器处理来自客户端的消息回执

      1. 创建一个IQDeliverConfirmHandler处理从客户端发送过来的IQ

package org.androidpn.server.xmpp.handler;

import org.androidpn.server.service.NotificationService;
import org.androidpn.server.service.ServiceLocator;
import org.androidpn.server.xmpp.UnauthorizedException;
import org.androidpn.server.xmpp.session.ClientSession;
import org.androidpn.server.xmpp.session.Session;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import org.dom4j.Element;

public class IQDeliverConfirmHandler extends IQHandler {
	private static final String NAMESPACE = "androidpn:iq:deliverconfirm";

	private NotificationService notificationService;

	public IQDeliverConfirmHandler() {
		// TODO Auto-generated constructor stub
		notificationService = ServiceLocator.getNotificationService();
	}

	@Override
	public IQ handleIQ(IQ packet) throws UnauthorizedException {
		// TODO Auto-generated method stub
		ClientSession session = sessionManager.getSession(packet.getFrom());
		IQ reply;
		if (session == null) {
			log.error("Session not found for key " + packet.getFrom());
			reply = IQ.createResultIQ(packet);
			reply.setChildElement(packet.getChildElement().createCopy());
			reply.setError(PacketError.Condition.internal_server_error);
			return reply;
		}
		if (session.getStatus() == Session.STATUS_AUTHENTICATED) {
			if (IQ.Type.set.equals(packet.getType())) {
				Element element = packet.getChildElement();
				String uuid = element.elementText("uuid");
				notificationService.deleteNotificationByUUID(uuid);
			}
		}

		return null;
	}

	@Override
	public String getNamespace() {
		// TODO Auto-generated method stub
		return NAMESPACE;
	}

}

      2. 修改IQRouter的构造方法,将IQDeliverConfirmHandler添加到IQHandler集合中

public IQRouter() {
        sessionManager = SessionManager.getInstance();
        iqHandlers.add(new IQAuthHandler());
        iqHandlers.add(new IQRegisterHandler());
        iqHandlers.add(new IQRosterHandler());
        
        // 将处理消息回执添加到IQHandler集合中
        iqHandlers.add(new IQDeliverConfirmHandler());
   }

      3. 修改NotificationManager中的sendNotificationToUser()方法 - 在PresenceUpdateHandler调用该方法时传入false,同时在其他类调该方法时传入true,

// 添加一个shouldSave标志位,防止在PresenceUpdateHandler中调用时删除Notification数据时重复保存该消息,导致消息没有得到删除
public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri, boolean shouldSave) {
	log.debug("sendNotifcationToUser()...");
	Random random = new Random();
	String id = Integer.toHexString(random.nextInt());
	IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
				uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		}
		// // 如果用户在线但不可用时,则保存推送消息到数据库中
		// else {
		// saveNotification(id, apiKey, username, title, message, uri);
		// }
	}
	// // 如果用户不在线但不可用时,则保存推送消息到数据库中
	// else {
	User user;
	try {
		// 通过用户名发送推送消息,在保存消息时,验证该用户名是否存储,防止存储无用数据
		user = userService.getUserByUsername(username);
		if (user != null && shouldSave) {
			saveNotification(id, apiKey, username, title, message, uri);
		}
	} catch (UserNotFoundException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	// }
}

  4. 在版本二的基础上修复离线发送逻辑

    1. 去掉NotificationManager中的sendNotificationToUser()方法中的shouldSave参数

    2. 在NotificationManager中添加一个发送离线消息给重新上线的用户

/**
* 从数据库中取出已有的离线消息推送给重新上线的用户
* 
* @param uuid
* @param apiKey
* @param username
* @param title
* @param message
* @param uri
*/
public void sendOfflineNotifcationToUser(String uuid, String apiKey, String username, String title, String message,
			String uri) {
	log.debug("sendNotifcationToUser()...");
	IQ notificationIQ = createNotificationIQ(uuid, apiKey, title, message, uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		}
	}
}

    3. 在PresenceUpdateHandler中修改process(Packet)方法的相关代码

if (session != null) {
	session.setPresence(presence);
	if (!session.isInitialized()) {
		// initSession(session);
		session.setInitialized(true);
	}

	// 遍历消息数据库
	List<Notification> list = notificationService.findNotificationsByUsername(session.getUsername());
	if (list != null && list.size() > 0) {
		for (Notification notification : list) {
			String apiKey = notification.getApiKey();
			String title = notification.getTitle();
			String message = notification.getMessage();
			String uri = notification.getUri();
			String uuid = notification.getUuid();
			notificationManager.sendOfflineNotifcationToUser(uuid, apiKey, session.getUsername(), title,
					message, uri);
		}
	}

}

-----------------------------------------华丽丽的分割线-----------------------------------------

 至此,基于Androidpn推送平台的离线功能就已经全部实现了

转载于:https://www.cnblogs.com/mattle/p/5034759.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值