`
autumnrain_zgq
  • 浏览: 61202 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

JavaPNS使用与吐槽

阅读更多
苹果平台开发的应用程序,不支持后台运行程序,所以苹果有一个推送服务在软件的一些信息推送给用户。
JAVA中,有一个开源软件,JavaPNS实现了Java平台中连接苹果服务器与推送消息的服务。但是在使用的过程中,有两点需要使用者注意一下,希望后续使用的同志们能避免我走过的覆辙。
1、一是向苹果的服务推送消息时,如果遇到无效的deviceToken,苹果会断开网络连接,而JavaPNS不会进行重连。苹果原文:
    If you send a notification and APNs finds the notification malformed or otherwise unintelligible, it returns an error-response packet prior to disconnecting. (If there is no error, APNs doesn’t return anything.) Figure 5-3 depicts the format of the error-response packet.
2、JavaPNS是一条条发送通知的,但是对于大规模发送的生产环境,显然是不可以的,建议使用批量发送。苹果原文:
The binary interface employs a plain TCP socket for binary content that is streaming in nature. For optimum performance, you should batch multiple notifications in a single transmission over the interface, either explicitly or using a TCP/IP Nagle algorithm.

对于此,我对JavaAPNS的代码进行了改动,使其支持批量发送和苹果断开重连,但是有一个问题需要大家注意一下,在正常发送的情况下,苹果是不会向Socket中写任何数据的,需要等待其读超时,this.socket.getInputStream().read(),确订推送结果的正常。通过持续的向Socket中写数据,实现批量发送,调用flush方法时,完成一次批量发送。在PushNotificationManager增加如下方法:
public List<ResponsePacket> sendNotification(List<PushedNotification> pnl) {
		logger.info(psn + "RR批量推送时消息体的大小为:" + pnl.size());
		List<ResponsePacket> failList = new ArrayList<ResponsePacket>();
		if (pnl.size() == 0) {
			return failList;
		}
		Set<Integer> sendSet = new HashSet<Integer>();
		int counter = 0;
		while (counter < pnl.size()) {
			try {
				this.socket.setSoTimeout(3000);
				this.socket.setSendBufferSize(25600);
				this.socket.setReceiveBufferSize(600);
				for (; counter < pnl.size(); counter++) {
					PushedNotification push = pnl.get(counter);
					if (sendSet.contains(push.getIdentifier())) {
						logger.warn("信息[" + push.getIdentifier() + "]已经被推送");
						continue;
					}
					byte[] bytes = getMessage(push.getDevice().getToken(), push.getPayload(), push.getIdentifier(), push);
					this.socket.getOutputStream().write(bytes);
					// 考虑到重发的问题
					// sendSet.add(push.getIdentifier());
				}
				this.socket.getOutputStream().flush();
				// 等待回馈数据,比单个发送时延时长一点,否则将无法获取到回馈数据
				this.socket.setSoTimeout(1000);

				StringBuffer allResult = new StringBuffer();
				ResponsePacket rp = new ResponsePacket();
				int readCounter = 0;
				// 处理读回写数据的异常
				try {
					logger.info(psn + "检查流数据是否可用:" + this.socket.getInputStream().available());
					byte[] sid = new byte[4];// 发送标记
					while (true) {
						int value = this.socket.getInputStream().read();
						if (value < 0) {
							break;
						}
						readCounter++;
						if (readCounter == 1) {
							rp.setCommand(value);
						}
						if (readCounter == 2) {
							rp.setStatus(value);
						}
						if (readCounter >= 3 && readCounter <= 6) {
							sid[readCounter - 3] = (byte) value;
							if (readCounter == 6) {
								rp.setIdentifier(ByteBuffer.wrap(sid).getInt());
								if (failList.contains(rp)) {
									logger.error("错误返馈数据中已经包含当前数据," + rp.getIdentifier());
								}
								failList.add(rp);
							}
						}
						allResult.append(value + "_");
					}
					this.socket.getInputStream().close();
				} catch (SocketTimeoutException ste) {
					logger.debug(psn + "消息推送成功,无任何返回!", ste);
				} catch (IOException e) {
					logger.debug(psn + "消息推送成功,关闭连接流时出错!", e);
				}
				logger.info("苹果返回的数据为:" + allResult.toString());
				if (readCounter >= 6) {
					// 找到出错的地方
					for (int i = 0; i < pnl.size(); i++) {
						PushedNotification push = pnl.get(i);
						if (push.getIdentifier() == rp.getIdentifier()) {
							counter = i + 1;
							break;
							// 从出错的地方再次发送,
						}
					}
					try {
						this.createNewSocket();
					} catch (Exception e) {
						logger.warn("连接时出错,", e);
					}
				}
			} catch (SSLHandshakeException she) {
				// 握手出错,标记不加
				logger.warn("SHE消息推送时出错,", she);
				try {
					this.createNewSocket();
				} catch (Exception e) {
					logger.warn("连接时出错,", e);
				}
			} catch (SocketException se) {
				logger.warn("SE消息推送时出错", se);
				counter++;
				try {
					this.createNewSocket();
				} catch (Exception e) {
					logger.warn("连接时出错,", e);
				}
			} catch (IOException e) {
				logger.warn("IE消息推送时出错", e);
				counter++;
			} catch (Exception e) {
				logger.warn("E消息推送时出错", e);
				counter++;
			}

			if (counter >= pnl.size()) {
				break;
			}
		}
		return failList;
	}

分享到:
评论
17 楼 langxuanlovehai 2015-08-22  
很好,学到了
16 楼 lixiangblue 2014-01-07  
在javapns2.2 中,重发确实存在bug。但是我发现。在注释重发后,notifications的返回结果会变得非常不准确(虽然不注释也不准确),所以根据系统的需求,如果需要比较准确的返回结果,还是不注销重发比较好,如果系统偏重与推送功能,对推送结果没有很高的要求,注释掉重发代码可以修复重复推送的bug。期待在以后的javapns版本中修复重复推送的bug。
15 楼 zhanghua_1199 2013-07-22  
在正常发送的情况下,苹果是不会向Socket中写任何数据的,需要等待其读超时。想问一下楼主,这个苹果读取超时是在哪看到的,能否给个链接?
14 楼 zhanghua_1199 2013-07-22  
lz,,,,你的
持续写入数据,遇到无效的deviceToken苹果会断开连接,并返回相应的消息编号,这时需要重新连接,再次发送数据
博文体现在哪里?是从这里开始吗:
if (readCounter >= 6) { 
                    // 找到出错的地方 



如果是,lz可以去看看javapns2.2版本的
PushNotificationManager.stopConnection()这个方法。这里面实现了重发。望回复。
13 楼 shanjh2000 2013-07-09  
请问,你的这个方法的输入参数: List<PushedNotification> pnl   怎么创建?
12 楼 autumnrain_zgq 2013-05-17  
lsqwind 写道
rock 写道
PushQueue支持重连,
源码:
private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
		try {
			Device device = notification.getDevice();
			Payload payload = notification.getPayload();
			try {
				payload.verifyPayloadIsNotEmpty();
			} catch (IllegalArgumentException e) {
				throw new PayloadIsEmptyException();
			} catch (Exception e) {
			}

			if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
			if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
			int identifier = notification.getIdentifier();

			String token = device.getToken();
			// even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
			BasicDevice.validateTokenFormat(token);
			//		PushedNotification pushedNotification = new PushedNotification(device, payload);
			byte[] bytes = getMessage(token, payload, identifier, notification);
			//		pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);

			/* Special simulation mode to skip actual streaming of message */
			boolean simulationMode = payload.getExpiry() == 919191;

			boolean success = false;

			BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			int socketTimeout = getSslSocketTimeout();
			if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
			notification.setTransmissionAttempts(0);
			// Keep trying until we have a success
			while (!success) {
				try {
					logger.debug("Attempting to send notification: " + payload.toString() + "");
					logger.debug("  to device: " + token + "");
					notification.addTransmissionAttempt();
					boolean streamConfirmed = false;
					try {
						if (!simulationMode) {
							this.socket.getOutputStream().write(bytes);
							streamConfirmed = true;
						} else {
							logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
						}
					} catch (Exception e) {
						if (e != null) {
							if (e.toString().contains("certificate_unknown")) {
								throw new InvalidCertificateChainException(e.getMessage());
							}
						}
						throw e;
					}
					logger.debug("Flushing");
					this.socket.getOutputStream().flush();
					if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");

					success = true;
					logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
					notification.setTransmissionCompleted(true);

				} catch (IOException e) {
					// throw exception if we surpassed the valid number of retry attempts
					if (notification.getTransmissionAttempts() >= retryAttempts) {
						logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
						notification.setTransmissionCompleted(false);
						notification.setException(e);
						logger.error("Delivery error", e);
						throw e;

					} else {
						logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
						//Try again
						try {
							this.socket.close();
						} catch (Exception e2) {
							// do nothing
						}
						this.socket = connectionToAppleServer.getSSLSocket();
						if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
					}
				}
			}
		} catch (CommunicationException e) {
			throw e;
		} catch (Exception ex) {

			notification.setException(ex);
			logger.error("Delivery error: " + ex);
			try {
				if (closeAfter) {
					logger.error("Closing connection after error");
					stopConnection();
				}
			} catch (Exception e) {
			}
		}
	}

我也看到这段代码了。确实是有重发功能,默认重发次数是3次。请教下楼主@ autumnrain_zgq 跟你写的这个方法比有哪些需要注意的地方?

这个代码正常情况下没有问题,遇到无效的deviceToken就无法重新发送信息了,还有,他不是批量提交信息的,
11 楼 lsqwind 2013-05-15  
rock 写道
PushQueue支持重连,
源码:
private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
		try {
			Device device = notification.getDevice();
			Payload payload = notification.getPayload();
			try {
				payload.verifyPayloadIsNotEmpty();
			} catch (IllegalArgumentException e) {
				throw new PayloadIsEmptyException();
			} catch (Exception e) {
			}

			if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
			if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
			int identifier = notification.getIdentifier();

			String token = device.getToken();
			// even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
			BasicDevice.validateTokenFormat(token);
			//		PushedNotification pushedNotification = new PushedNotification(device, payload);
			byte[] bytes = getMessage(token, payload, identifier, notification);
			//		pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);

			/* Special simulation mode to skip actual streaming of message */
			boolean simulationMode = payload.getExpiry() == 919191;

			boolean success = false;

			BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			int socketTimeout = getSslSocketTimeout();
			if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
			notification.setTransmissionAttempts(0);
			// Keep trying until we have a success
			while (!success) {
				try {
					logger.debug("Attempting to send notification: " + payload.toString() + "");
					logger.debug("  to device: " + token + "");
					notification.addTransmissionAttempt();
					boolean streamConfirmed = false;
					try {
						if (!simulationMode) {
							this.socket.getOutputStream().write(bytes);
							streamConfirmed = true;
						} else {
							logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
						}
					} catch (Exception e) {
						if (e != null) {
							if (e.toString().contains("certificate_unknown")) {
								throw new InvalidCertificateChainException(e.getMessage());
							}
						}
						throw e;
					}
					logger.debug("Flushing");
					this.socket.getOutputStream().flush();
					if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");

					success = true;
					logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
					notification.setTransmissionCompleted(true);

				} catch (IOException e) {
					// throw exception if we surpassed the valid number of retry attempts
					if (notification.getTransmissionAttempts() >= retryAttempts) {
						logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
						notification.setTransmissionCompleted(false);
						notification.setException(e);
						logger.error("Delivery error", e);
						throw e;

					} else {
						logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
						//Try again
						try {
							this.socket.close();
						} catch (Exception e2) {
							// do nothing
						}
						this.socket = connectionToAppleServer.getSSLSocket();
						if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
					}
				}
			}
		} catch (CommunicationException e) {
			throw e;
		} catch (Exception ex) {

			notification.setException(ex);
			logger.error("Delivery error: " + ex);
			try {
				if (closeAfter) {
					logger.error("Closing connection after error");
					stopConnection();
				}
			} catch (Exception e) {
			}
		}
	}

我也看到这段代码了。确实是有重发功能,默认重发次数是3次。请教下楼主@ autumnrain_zgq 跟你写的这个方法比有哪些需要注意的地方?
10 楼 autumnrain_zgq 2013-05-09  
sorehead 写道
autumnrain_zgq 写道
sorehead 写道
楼主可以贴一份完整的代码吗,我也在纠结这个问题。

下载下来JavaAPNS源代码项目,在PushNotificationManager这个类中增加我文章中增加的那个方法就可以了。其它地方我也没有改的,

 this.createNewSocket();  都做了些什么事情?只是重新建立一个socket连接吗?方便贴出这个方法吗?
                

	public void createNewSocket() throws SocketException, KeystoreException, CommunicationException {
		logger.info(psn + "创建新的Socke的连接");
		if (this.socket != null) {
			try {
				socket.close();
			} catch (Exception e) {
				logger.info("关闭Socket连接时出错...", e);
			}
		}
		this.socket = connectionToAppleServer.getSSLSocket();
		this.socket.setSoTimeout(3000);
		this.socket.setKeepAlive(true);
	}

9 楼 sorehead 2013-05-08  
autumnrain_zgq 写道
sorehead 写道
楼主可以贴一份完整的代码吗,我也在纠结这个问题。

下载下来JavaAPNS源代码项目,在PushNotificationManager这个类中增加我文章中增加的那个方法就可以了。其它地方我也没有改的,

 this.createNewSocket();  都做了些什么事情?只是重新建立一个socket连接吗?方便贴出这个方法吗?
                
8 楼 autumnrain_zgq 2013-05-08  
sorehead 写道
楼主可以贴一份完整的代码吗,我也在纠结这个问题。

下载下来JavaAPNS源代码项目,在PushNotificationManager这个类中增加我文章中增加的那个方法就可以了。其它地方我也没有改的,
7 楼 sorehead 2013-05-08  
楼主可以贴一份完整的代码吗,我也在纠结这个问题。
6 楼 rock 2013-04-24  
PushQueue支持重连,
源码:
private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
		try {
			Device device = notification.getDevice();
			Payload payload = notification.getPayload();
			try {
				payload.verifyPayloadIsNotEmpty();
			} catch (IllegalArgumentException e) {
				throw new PayloadIsEmptyException();
			} catch (Exception e) {
			}

			if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
			if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
			int identifier = notification.getIdentifier();

			String token = device.getToken();
			// even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
			BasicDevice.validateTokenFormat(token);
			//		PushedNotification pushedNotification = new PushedNotification(device, payload);
			byte[] bytes = getMessage(token, payload, identifier, notification);
			//		pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);

			/* Special simulation mode to skip actual streaming of message */
			boolean simulationMode = payload.getExpiry() == 919191;

			boolean success = false;

			BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			int socketTimeout = getSslSocketTimeout();
			if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
			notification.setTransmissionAttempts(0);
			// Keep trying until we have a success
			while (!success) {
				try {
					logger.debug("Attempting to send notification: " + payload.toString() + "");
					logger.debug("  to device: " + token + "");
					notification.addTransmissionAttempt();
					boolean streamConfirmed = false;
					try {
						if (!simulationMode) {
							this.socket.getOutputStream().write(bytes);
							streamConfirmed = true;
						} else {
							logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
						}
					} catch (Exception e) {
						if (e != null) {
							if (e.toString().contains("certificate_unknown")) {
								throw new InvalidCertificateChainException(e.getMessage());
							}
						}
						throw e;
					}
					logger.debug("Flushing");
					this.socket.getOutputStream().flush();
					if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");

					success = true;
					logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
					notification.setTransmissionCompleted(true);

				} catch (IOException e) {
					// throw exception if we surpassed the valid number of retry attempts
					if (notification.getTransmissionAttempts() >= retryAttempts) {
						logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
						notification.setTransmissionCompleted(false);
						notification.setException(e);
						logger.error("Delivery error", e);
						throw e;

					} else {
						logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
						//Try again
						try {
							this.socket.close();
						} catch (Exception e2) {
							// do nothing
						}
						this.socket = connectionToAppleServer.getSSLSocket();
						if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
					}
				}
			}
		} catch (CommunicationException e) {
			throw e;
		} catch (Exception ex) {

			notification.setException(ex);
			logger.error("Delivery error: " + ex);
			try {
				if (closeAfter) {
					logger.error("Closing connection after error");
					stopConnection();
				}
			} catch (Exception e) {
			}
		}
	}
5 楼 autumnrain_zgq 2013-04-03  
鐜嬫旦 写道
鐜嬫旦 写道
pns 2.2版本不是支持群发嘛?

楼主,看到尽快回复下,,最近也在整这个

你好,是不支持群发的,群发的情况下,是在一个连接打开的情况下,持续写入数据,遇到无效的deviceToken苹果会断开连接,并返回相应的消息编号,这时需要重新连接,再次发送数据。博文的代码中这是这样实现的,
4 楼 鐜嬫旦 2013-04-02  
鐜嬫旦 写道
pns 2.2版本不是支持群发嘛?

楼主,看到尽快回复下,,最近也在整这个
3 楼 鐜嬫旦 2013-04-02  
pns 2.2版本不是支持群发嘛?
2 楼 autumnrain_zgq 2013-03-27  
linbaoji 写道
请问你的 JavaPNS 版本是什么 啊!??
谢谢1

你好,我查看一下,版本是:2.2
1 楼 linbaoji 2013-03-20  
请问你的 JavaPNS 版本是什么 啊!??
谢谢1

相关推荐

    .net 三层架构制作吐槽网

    在设计数据库时,开发者需要考虑表之间的关系,如用户和吐槽之间的多对多关系,以及点赞和评论与吐槽的一对多关系。 在实现这些功能时,还需要考虑安全性。例如,用户登录时应验证凭证,防止SQL注入攻击;在发布...

    Qnmlgb吐槽网站

    一个留言类型的网站,适合新手。吐槽网站。特别适合新手进行学习。也可以直接使用。

    基于PHP的消息果留言板(吐槽版) PHP源码.zip

    基于PHP的消息果留言板(吐槽版) PHP源码.zip

    基于Django的校园食堂菜评价与吐槽互动平台设计源码

    本项目是一款基于Django框架开发的校园食堂菜评价与吐槽互动平台设计源码,包含201个文件,涵盖45个JavaScript文件、38个GIF动画文件、30个CSS样式文件、26个Python源代码文件、17个PNG图片文件、12个HTML文件、10个...

    ssm吐槽论坛毕业设计程序

    采用java技术构建的一个管理系统。整个开发过程首先对系统进行需求分析,得出系统的主要功能。接着对系统进行总体设计和详细设计。总体设计主要包括系统功能设计、系统总体结构设计、系统数据结构设计和系统安全设计...

    工作中那些不得不吐槽的Chinglish

    工作中那些不得不吐槽的Chinglish

    无力吐槽是什么意思.doc

    【流弊】这个词具有两层含义,一方面指事情的弊端或不良影响,另一方面在网络语境中,它作为“牛B”的替代词,有时带有卖萌或俏皮的意味,尤其在某些地方方言中,"流弊"与"牛B"发音相似,因此被广泛使用。...

    吐槽哥端口扫描器 v3.9

    一个快速高效的端口扫描工具,制定IP段扫描端口,还可以对部分路由器读取路由器相关用户信息。支持路由器或服务器型号:XM-3300N- ASUS- D-Link- LevelOne- Netis- Pozitron- TP-LINK支持端口:8080,80,8888,8081...

    吐槽:web登录页面(by Alan)

    web登录页面

    安卓直播视频播放流媒体IPCameraRTSPDLNA相关-android实现吐槽弹幕.rar

    在Android应用中,可以使用RTSP协议与IP Camera通信,获取并播放其视频流。 4. **RTSP(Real-Time Streaming Protocol)**:这是一种控制协议,用于在客户端和服务器之间管理多媒体数据传输。在Android上,RTSP常...

    一起来吐槽-crx插件

    可以在任何网页的任何地方...除了漫画、图片,任何的网页都是可以使用此扩展进行吐槽评论的。 ***另外请注意*** 安装本扩展后,那些已经打开的页面,需要重新刷新一次才会出现吐槽栏。 支持语言:English,中文 (简体)

    支付宝钱包十大最烂文案——吐槽支付宝ppt模板.rar

    【支付宝钱包十大最烂文案——吐槽支付宝PPT模板】是一个针对支付宝软件的用户界面和文案设计进行批评与分析的资源。这份PPT模板旨在通过简约而精美的设计,揭示出支付宝在用户体验方面可能存在的问题,特别是针对其...

    项目实战SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序

    首页:展示企业红黑榜Top、最新发布的企业吐槽和问题。 红黑榜:分页展示高分和低分企业。 发布:允许用户对企业进行吐槽或提问。 问题列表与详情:展示所有提问及其回复。 我的:包括个人资料管理、已发布吐槽和...

    Flex饼图向上吐槽

    Flex饼图向上吐槽是一个关于Adobe Flex中饼图组件的专题,这个组件被设计用来以图形化的方式展示数据,尤其适用于显示部分与整体的关系。在Flex中,饼图是一种常见的图表类型,它将数据集中的各个数据项以扇形区域...

    (正文格式)12-14基于节目创作视角谈《吐槽大会》成功的关键因素.zip

    1. **创新的节目形式**:《吐槽大会》打破了传统的娱乐节目模式,引入了美式脱口秀的“Roast”元素,将喜剧与批判性评论相结合,形成了独特的“吐槽”风格,这种新颖的形式吸引了大量观众的关注。 2. **话题性与...

    基于Vue、JavaScript、HTML的bkb-uniapp企业黑红名单吐槽小程序设计源码

    该小程序通过吐槽发布企业信息,为打工人提供评判企业好坏的平台,评判标准自定,便于辨别企业优劣。技术栈包括SpringBoot、MybatisPlus、uniapp和uview2等先进组件,代码注释丰富,结构简洁,易于上手。适合项目...

    吐槽:http://www.tucao.tv吐槽第三方Android客户端

    吐槽 特色 首页六大模块,推荐,新番,影剧,游戏,动画,频道 全站排行榜,支持每日/每周排序 放映时间表,可以查看周一到周日新番的更新情况 频道列表,支持按照发布时间/播放量/弹幕排序 视频搜索,支持分频道...

    直面吐槽 智能家居产品.pdf

    直面吐槽 智能家居产品.pdf

    《吐槽CSDN的渲染》的Markdown源码

    《吐槽CSDN的渲染》的Markdown源码

Global site tag (gtag.js) - Google Analytics