商户支付方式梳理

近期在做另一个项目,涉及支付相关的功能。原来我主要做对公相关的业务,现在发现对私的支付手段已经日新月异,这很大程度上是由于近几年第三方支付公司的影响,对大家的支付习惯带来了很大变革。这篇文章主要介绍作为一个商户需要考虑的支付方式有哪些,每种支付方式的业务关注点有哪些。

支付从大类上分为线上支付、线下支付两大类。线上支付大家都比较熟悉,典型的场景是网上电商平台,包括京东、天猫、美团等等;线下支付大家应该也比较清楚,比如线下的麦当劳、肯德基、好利来、百货商店、一些小摊贩等。区分界限也很明确,线上就是自己不是在实体店中,线下就是在店铺内部或是面对面的情况。但现在随着各个行业互联网化的进行,线上线下的界限开始越来越模糊,有些甚至开始出现重叠。

线上支付

1.网银页面支付

顾名思义就是指的通过网银跳转的方式进行的。典型的就是在12306的网站上,选择建行卡支付,然后跳转到建行的网银支付页面,客户输入账号、密码或手机验证码,完成支付。这种支付方式只需要开通网上银行或是通过账号形式支持。这种方式除了可以跳转到各家银行的网银页面外,还可以跳转到银联、财付通、支付宝等的支付页面进行支付。

值得一提的是为了简化商户的开发成本(与不同家的网银进行对接),商户可以只与第三方支付公司对接,然后由第三方支付公司来和各家银行对接。当然了清算也是由第三方支付公司来和商户进行清算。

2.扫码支付(主扫支付)

另一种现在很流行的支付方式就是线上扫码支付,又叫主扫支付,指的是客户主动通过微信、支付宝等客户端扫码网页上的二维码来完成支付。扫码支付的流程可以参考下面的资料当面付产品介绍,需要商户系统先把订单信息发送到微信支付宝等,微信支付宝会返回对应的二维码字符串,商户将二维码转换为图片展示到界面上。二维码相关的资料我会在另一个文章中单独来写,总的来说二维码是一个字符串,各个APP会对这些字符串进行解析。

支付宝主扫流程图

刚开始各家第三方支付公司推出的二维码之间是不兼容的,如果一个商户要支持多种支付方式,比如微信、支付宝、银联等,就需要在页面上展示多个二维码,很麻烦。为了解决这个问题,就有了聚合支付的概念,就是说一个二维码同时支持多种支付方式,如何做呢?其实也很简单,刚说了二维码本身就是个字符串,只要让这个字符串被多个支付APP认就行了,目前URL形式的字符串很多APP都可以识别。这些URL二维码微信支付宝等扫描后会展示对应的网页,这个页面一般是HTML5页面,我们可以在这个页面上提供多种支付方式,具体的话不同支付APP有不同的接口规范。这就是聚合支付的雏形,商户自己可以做这种聚合支付,也可以由专门的聚合支付提供商来做。当然了有些聚合支付商直接把自己作为一个大商户,然后再和商户进行清算,这就是二清了,这是非法的,因为聚合支付商并不具有清算的资质。

为了更进一步的规范二维码支付,银联推出了银联的聚合支付码,可以支持所有的第三方支付的APP、各个银行的APP、银联的云闪付等。这个二维码是怎么做的呢,本质上银联的二维码也是个URL,只是这个URL不是个页面,如果微信支付宝等第三支付APP扫描的话银联会返回一个跳转的URL,然后客户可以在这个URL完成支付(后续处理和聚合支付相同)。如果是银行或是银联的APP当遇到这种格式的URL会调用银联的接口对URL进行解析,解析出商户信息、订单信息,然后展示到界面上,客户支付即可。 上面说的动态二维码是有时效性的,这也是为了防止一些长期失效的订单被错误支付。

3.APP跳转支付

如果商家有自己的APP,客户在下单后选择支付就不能使用扫码二维码的方式(没法扫自己的屏幕),这时一种处理方式就是跳转到指定的支付APP,如微信、支付宝等,然后再其对应的支付页面进行支付。不同的支付APP有不同的支付SDK包及接口说明,大家可以看下。

线下支付

1.POS刷卡方式

这种是最常见的形式了,我们在商场购物可以进行刷卡,包括借记卡、贷记卡等,境内一般只要是银联的卡都可以,如果是境外的话就需要是Mastercard、VISA了。这种首先商户需要是某个银行的商户(收单行),也可以直接是银联的商户。如果刷的卡和商户收单行不同,还会涉及到银联的清算,银联会收取对应的手续费。

2.线下被扫模式

这里说的被扫是指的客户展示付款码,然后商户通过扫码枪、智能POS或是扫码盒子来扫描此付款码完成支付。这种模式也是目前比较推荐的,效率也高,更特别的是客户甚至在支付时不需要有网络。具体流程如下(以支付宝为例)

支付宝被扫流程图

被扫模式一般是客户选择支付方式,是微信、还是支付宝,然后商家扫码,扫码后商家会把订单信息联通此付款码一并发送到微信或支付宝的后台完成扣款。

由于支付码是动态的,安全性较高,而且使用便捷,也是后续推荐的支付方式。

3.现金或扫描静态二维码

静态二维码就是现在很多小商贩上贴的二维码,微信、支付宝的。其本质是对应到某个微信支付宝的账号。当扫码时就相当于给他做了一笔转账。由于是静态二维码所以不涉及对应的订单信息,所以在支付时需要输入金额。由于静态二维码风险较高,目前人行对每日的限额已经有了限制。

4.线下主扫动态码支付

这种现在用的不多,大家应该也遇到过,在线下的时候,商户会再一个屏幕或是POS上展示一个二维码,然后由我们拿微信或支付宝来扫码。这个二维码本质上和线上的动态二维码没区别,只是展示的形式不同而已。这个动态二维码的好处是可以包含订单信息,不需要客户输入金额,而且动态码也有时效性,比较安全。

上面简单将线上线下的支付方式进行了介绍,作为一个商户不需要支持所有的这些支付方式,只要有选择的支持即可。另外由于各个渠道的支付手续费不同,通道服务质量不同,商户在收银台展示不同支付方式的时候可以有所侧重,进而引导客户的支付行为。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

二维码小工具

现在大家随处可以看到二维码,二维码是相对于原来的一维条形码来说的,二维码的种类很多,常见的包括QR CodePDF417码等。我们目前经常看到的微信支付宝的付款码等都是QR code,QR Code相比其他二维码有很多优势,包括容错性,快速响应性等,它支持40种尺寸,从21*21177*177。随着尺寸的增大,每个二维码保存的信息量也随之增大,但也有个问题就是手机扫描识别的成功率会降低,特别的对于大尺寸的码很多程序是不识别的。关于QR Code大家可以参考这篇文章,写的很棒。

二维码本质是一种保存信息的载体,它可以保存二进制、数字、字符等,不同类型编码方式不同,保存的信息量略有区别。二维码的存在也使得信息快速传递成为可能,相比原来的输入一长串网址,扫描一个二维码要快捷准确的多,这方面二维码相对于目前的图像识别也要更准确、容错性更高。你可以利用二维码保存、传递很多东西,限制你的只是你的想象力。

近期突发奇想,想通过二维码来传递大数据量的信息,找了相关资料后,发现单个二维码容量是有限制的,具体可以参考这个网站,例如VERSION 20的二维码可以保存1600个数字,保存970个字符,具体保存的多少还和容错率有关。为了实现通过二维码传递大量字符的目的,可以按照下面步骤来做:

  1. 将原始信息进行编码,可以采用UTF-8或其他格式。
  2. 将原始信息拆分为固定大小的子串,并且标明子串的序号(便于后续还原)。简单的方式可以在字符串开头几位用于表示顺序。
  3. 将这些子串编码,生成二维码。

上述过程是可行的,但有一个问题是由于大尺寸的二维码识别率不高,只能采用小尺寸的,但小尺寸的保存的信息又少,这样就导致生成的图片过多。为了解决这个问题,可以在一个图片中放多个二维码,这样的话可以提高保存信息量,并且保证每个二维码识别率;在还原的时候加一个预处理,将图片进行拆分即可。

针对上面说的内容我写了个java小程序,大家可以在这里查看使用,其中使用的是zxing的库。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

可靠消息模式

近期项目的事一直很忙,发现要学习只能自己挤时间。最近大致看了下淘宝曾宪杰写的《大型网站系统与Java中间件实践》,感觉还是受益匪浅。这里结合自己的体会把可靠消息这块总结一下。

可靠消息,顾名思义就是在前置交易成功后系统能保证一定把消息发送出去,另一方面,如果前置交易失败,消息一定不能发送。可靠消息和事务交易还是有区别的,事务消息一般是通过事务管理器来保证消息系统和应用服务的数据库系统的一致性。由于消息系统和数据库系统一般是两个系统,所以事务消息一般采用两阶段提交来做,而两阶段提交在异常情况下问题很多,实际系统一般很少采用。可靠消息是事务消息的简化版,它提供了最终的一致性,同时有避免了厚重的事务管理器。

注: 本文介绍的可靠消息专指发送端一致性的保证,即保证消息一定能发送到消息中间件或接收端。对于接收端的消息一致性,即接收端一定能收到并处理消息不做阐述。接收端消息一致性的实现方式有很多,很多消息中间件的文档中都会提到,包括但不限于如下两种(1)接收端消息处理成功才认为消息接收成功,接收端进行幂等处理(进行判重或业务逻辑支持重复);(2)接收端采用拉取方式从中间件主动获取消息。

下面简单介绍一下可靠消息的实现方式:

1、本地事务方式

这种是最简单的方式,即在同一个本地事务中将待发送的消息放在单独的表中,并更新(或新增)前置交易的状态。详细处理过程如下:

联机交易处理过程:

  1. 启动事务 1.1 记录“待发送消息表”,状态为“待发送” 1.2 完成业务处理,更新(或新增)前置交易的状态,例如记录业务流水等
  2. 提交事务
  3. 进行异步消息发送功能。在异步发送消息确认成功后,将“待发送消息表”状态改为“发送成功”。

异常情况处理:

  • 启动自动任务定时扫描“待发送消息表”中的长期“待发送”的消息,然后进行消息发送。
  • 对于多次重试发送消息均失败的情况(可能消息系统故障,或应用程序问题导致消息格式不正确等),将“待 发送消息表”状态置为“发送失败”,后续由人工处理。

上述这种本地事务方式的优点是:

  1. 由本地事务保证发送消息和记录交易状态的一致性,而无需应用提供单独的查询交易确定交易的状态;
  2. 相比后面处理方案,无需考虑应用本身阻塞的问题,可保证消息永远不会漏发;
  3. 由自动任务保证消息最终一定会发送成功,从而保证了最终一致性。

缺点是:

  1. 需要保证消息待发送表和业务流水信息表在一个数据库中,在分布式环境下,相当于每个数据库上都需要有这样一套表;
  2. 需要应用本身支持事务,且需要应用明确记录待发送表,或通过框架隐式将业务更新和消息记录进行事务耦合;
  3. 需单独的自动任务对待发送消息表进行轮训、更新,增加了数据库的开销。

2、预发送及确认机制

这是曾宪杰书中提到的方式,即在应用记录前置交易状态之前、之后反别发送预发送消息、确认发送消息。详细处理过程如下:

联机交易处理: 1. 发送预发送消息:接收方一般为消息中间件或其他单独消息服务,此系统需保证持久化后再返回成功。消息中需包含应用交易标示信息,以便能在后面确认应用是否处理成功。 2. 完成业务处理,更新(或新增)前置交易的状态,例如记录业务流水等。 3. 发送确认消息:接收方收到消息后进行消息发送,并更新本地的消息状态为发送成功。此步骤可以异步来做,以减少联机交易等待时间。

异常情况处理:

  • 消息中间件或消息服务对于长时间处于待发送的消息,需要调应用提供的查询交易确认实际交易结果,如实际交易成功则发送消息,如失败则取消发送。
  • 需要注意应用查询交易返回“处理中”或“无此流水”的情况,这时由于应用本身可能存在阻塞,故不能盲目将消息置为失败。建议置入异常消息,进行人工处理,或设置很长的超时时间,超时后置为失败。
  • 对于消息服务实际发送消息时的失败,需要重试进行发送,对于长时间发送均失败的情况,需要置入异常队列,由人工处理。

上述处理方式的优点是:

  1. 只需要应用增加一个预发送的消息,即可保证消息可靠性
  2. 由单独的消息中间件来保证消息发送,避免了与应用过多耦合
  3. 可以严格保证消息的最终一致性,而无需引入事务的开销
  4. 可扩展性好,各组件功能切分明确,对应用侵入性小

缺点是:

  1. 当应用阻塞时无法断定交易即为失败,超时时间的设定有经验的因。为防止事务挂起,异常情况只能人工确认。系统无法做到消息发送和业务功能的强一致。
  2. 应用需额外开发查询交易,来确认交易的结果。此交易需要保证幂等性。

3、接收方定期校对方式

当发送方无法或不愿意进行改动以保证消息一致性时,可以通过接收方的定期校对来保证消息的最终一致性。大体实现逻辑如下:

联机交易处理:

  1. 完成业务处理,更新(或新增)前置交易的状态,例如记录账务流水等。
  2. 发送消息。(发送消息可能会丢失,发送方不进行任何保证)。接受方收到消息进行相应处理(异步)。

异常情况处理:

  • 当接收方没有收到消息时或收到消息但业务处理异常时,需要根据定时策略,定时调用发送方的查询交易进行查询并校对,对遗漏的消息进行补偿处理。

上述处理方式的优点:

  1. 对发送方无约束,其可以不保证发送消息一定发送成功;
  2. 可以适用于消息发送渠道不可靠或即使收到消息也无法保证消息被处理的情况。比如移动手机端的消息推送;
  3. 无需消息中间件支持。

缺点是:

  1. 定期校对功能的实现开销,比对哪些消息没处理当交易量大时并不是一个简单的工作。
  2. 需要发送方提供业务查询或下载的功能,以便确认漏处理的消息。
  3. 接收方需要进行消息幂等处理(其实不算缺点了,上面两个方案也需要进行此保证)

上面简单介绍了可靠消息的设计思路,大家可结合自身应用的特点进行选择。可靠消息本身是有额外开销的,我们能做的只能尽量减少对应用的影响。实际中消息中间件的实现还有其他考虑的内容,比如消息存储、性能、应用如何处理中间件故障等等,大家可以参考曾宪杰书中的内容。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

你应该知道的时间知识

时间是一个大家既熟悉又陌生的概念,大家每天都在用,但对于时间大家又有很多不了解的内容,比如闰秒、时区、夏令时等等。最近在做全球支付的项目,在项目中也遇到一些关于时间的问题,比如时间的保存、展示、查询等等。自己抽空查了很多资料,在此把我了解的和大家分享下,也希望对大家有帮助。

如果你觉得时间很简单,可以先看一下这个文章《你确信你了解时间吗?》,本文稍后也会说下这篇文章中提的事情。时间的处理是很复杂的,里面有很多坑,下面简单罗列几项:

  1. 闰年的规则:规则很复杂,不过好在有规则
  2. 时区的规则:一段时间内规则明确,如果考虑历史就呵呵了
  3. 夏令时的规则:跟各个地区的当地政策有关,既然是政策就不能公式化了
  4. 闰秒的规则:是随机加的,会临时通知的,我们都不知道哪天会加下一个闰秒

时间的定义

首先时间是什么呢,你会发现没有一个合适的定义,维基百科提到一个争议较小的定义为“时间是时钟量测的物理量”。那么时间是如何测量的呢,很早之前很多时间单位都是用天文概念定义的,包括。由于年是使用地球绕太阳转的周期定义,月采用月球绕地球公转周期定义,日采用地球自转周期来定义,但这几个周期都是相对独立的,各自之间的换算都不是整数,例如一年并不都是365天,回归年(太阳年)=365.242199174日,所以就有了闰年的概念,有些年份的2月会多1天,为29天。目前使用的闰年规则为: + 4的倍数是闰年。 + 100的倍数不是闰年。 + 400的倍数是闰年

2000年千年虫的问题其中一个就是因为这个闰年问题,因为2000年是闰年,但很多程序没有考虑到这个场景(另一个问题是采用2位数值来保存年而引起了处理错误)。月的概念在这就不说了,因为它已经偏离最初的定义太多了,中国的农历应该是比较接近自然定义的,但那个太复杂了,-_-

秒的定义和闰秒

下面再说下秒的定义,最开始秒的定义是太阳日(或地球自转)的1/86400,但随着近代测量越来越精确,发现这种天文秒的精度太低了,而且地球自转的周期本身的也是波动的,近代观测结果是逐渐变慢的,这样就无法提供一个准确的秒了。在1967年第13届国际计量大会上通过一项决议,重新对秒进行了定义,定义为铯-133原子基态两个超精细能级间跃迁辐射9,192,631,770周所持续的时间,也就是我们现在说的原子时。原子时的定义是近代的一个重大进步,目前原子钟的精度可以达到每2000万年才误差1秒,此精度还在不断提高中。你可能觉得时间精度和我们生活关系不大,这你就错了,我们每天赖以使用的GPS核心原理就是原子时,我们的手机是根据天空中的多个卫星发送的位置(卫星的位置)和时间信号,然后跟自己的时间通过复杂的公式,来计算出自己的当前位置。随着原子钟的时间精度提高,定位的精度从石英钟时代的14米减小到了现在的2.9米。

秒被重新定义为原子时后(目前我们所用的时间UTC也采用原子时),由于原子钟的精度很高,比原来使用的天文秒要精确很多。闰秒就是为了校正原子秒和天文秒的误差而存在的。因为天文秒是根据地球自转来确定的,但是地球自转的周期不是固定的,它本身是有波动的,这样就到这一天不再是86400秒了,可能会有些误差,现在规定如果误差达到0.9秒就需要进行调整,会在协调世界时(UTC,在时刻上和格林威治时间GMT尽量一致)的6月30日、或12月31日的23:59:59秒后再加上1秒,也就是23:59:60。目前只有增加闰秒,是因为地球自转在减慢,由于地球自转的波动是随机的,也就导致闰秒是随机公布的,谁也不知道下次是什么时候。加入随机的闰秒后,一分钟可能是61秒或60秒。因此在UTC系统的时间尺度中,秒和比秒小的单位(毫秒、微妙等)其长度是固定的,但是对于分钟和比分大的单位(小时、天、周等)的长度都是可变的。闰秒给大家带来了很多困扰,例如日本,由于其时区为UTC+09:00,所以闰秒调整会出现在上午9点,这个时候让你调表是个麻烦事,所以日本强烈建议取消闰秒。闰秒带来的另一个问题是计算机使用的时间,这里需要说一下:大家都知道时间是在计算机中是表示为一个long整数的,但是闰秒是不计算在现在的时间戳内的,也就是long整数中是没有这1秒的,这样在闰秒后操作系统时间会出现倒流,这个原因曾导致过程序崩溃,对此不同系统处理各异,Linux会让后续时间变慢以适应时间倒流,Windows则直接忽略。闰秒的另一个问题就是导致时长精度问题,如果你直接拿两个时间戳相减得到的不是真正的秒数,还需要加上这段时间发生的闰秒数。

下面是历史上所有闰秒统计:

实行年份 6.30 23:59:60 12.31 23:59:60 实行年份 6.30 23:59:60 12.31 23:59:60
1972年 +1秒 +1秒 1989年   +1秒
1973年   +1秒 1990年   +1秒
1974年   +1秒 1992年 +1秒  
1975年   +1秒 1993年 +1秒  
1976年   +1秒 1994年 +1秒  
1977年   +1秒 1995年   +1秒
1978年   +1秒 1997年 +1秒  
1979年   +1秒 1998年   +1秒
1981年 +1秒   2005年   +1秒
1982年 +1秒   2008年   +1秒
1983年 +1秒   2012年 +1秒  
1985年 +1秒   2015年 +1秒  
1987年   +1秒 2016年   +1秒

时区和夏令时

时区和夏令时这两个概念更是个政治概念了。你可以看下世界时区的划分,发现很乱,这里面很多政治因素在里面,我们需要知道的就是各个时区是相对协调世界时UTC做了调整,例如我国是UTC+08:00。在处理时区时要指定它的地区名称,而不是国家(一个国家可能跨多个时区,例如美国),也不是位置,虽然时区和位置有关,但不是线性的。说完了时区还要再说下夏令时,夏令时又叫日光节约时间(Daylight saving time,或Summer time),是为了节约能源让大家在夏天早起1小时(也就是把时钟拨快1小时),各个地区对于夏令时有不同的规定,而且不同年份的开始日期还可能不同(跟政府政策有关)。我国在1986-1991年实行夏令时,后来因为中国地域辽阔且修改事件效果不好且影响大家生活,后来作废了。

程序处理应注意的内容

好了,到这时间的概念大家应该都清楚了。下面再说下程序中时间处理中应该注意的。

  1. 数据库中存储的时间、操作系统的时间都是一个long整数,它是指格林威治时间1970年01月01日00时00分00秒起至现在的总秒数(或是总毫秒数);
  2. 只有在展示时才将此时间戳换算成本地时间,换算时需要考虑时区和夏令时。当然了如果你直接将本地时间字符串存到数据库中,那很多转换工作需要你自己做了,需要考虑清楚。在多时区处理时一般将UTC时间存入数据库,便于后续处理。
  3. 时间包括两个属性,一个是时间戳,即相对于UTC的某个时点的偏移量;另一个是时区,即事件发生是所在的时区信息,这个信息可以根据你的业务场景分析是否需要
  4. 在java中如果你输出显示时间,默认是本地时间,和系统设置有关,如果要输出某个时区的时间需要自己指定时区;另外这个本地时区时间会考虑夏令时,当然了因为夏令时和政府政策有关,java不可能实时获知这些信息,如果你想自己控制,则需要建立一个自己专用的夏令时起止时间表,自己进行换算(我们海外系统就是自己保存的夏令时信息),这时请不要再使用Asia/Shanghai这种,请使用UTC+08:00这种,会避免程序自身夏令时的处理影响。

下面给大家展示一个测试程序,通过这个能看出java程序的处理方式。值得一提的是上面酷壳提到的这个时间现在并不特殊,反而另一个时间是特殊的(1900-12-31 23:54:16),我不是说那篇文章的不对,而是很可能是近期政府对历史的时间定义有调整。

package com.zyh.mytest;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class TimeTest {

	private static final String TIME_FORMAT_STR = "yyyy-MM-dd HH:mm:ss";

	private SimpleDateFormat utc8Sdf;
	private SimpleDateFormat utcSdf;
	private SimpleDateFormat shanghaiSdf;

	@Before
	public void setUp() {
		utc8Sdf = new SimpleDateFormat(TIME_FORMAT_STR);
		utc8Sdf.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));

		utcSdf = new SimpleDateFormat(TIME_FORMAT_STR);
		utcSdf.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));

		// 输出全部时区
		// for (String s : TimeZone.getAvailableIDs()){
		// System.out.println(s);
		// }
		shanghaiSdf = new SimpleDateFormat(TIME_FORMAT_STR);
		shanghaiSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
	}

	/**
	 * 测试时区的时差
	 * 
	 * @throws ParseException
	 */
	@Test
	public void testTimeZone() throws ParseException {
		Date utc8Dt = utc8Sdf.parse("2017-10-01 13:04:56");
		Assert.assertEquals(1506834296000L, utc8Dt.getTime());

		Date utcDt = utcSdf.parse("2017-10-01 13:04:56");
		Assert.assertEquals(1506863096000L, utcDt.getTime());

		Assert.assertEquals((long) (8 * 60 * 60 * 1000), utcDt.getTime() - utc8Dt.getTime());// 8H

	}

	/**
	 * 测试夏令时
	 * 
	 * @throws ParseException
	 */
	@Test
	public void testDst() throws ParseException {

		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
		sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
		Date shanghaiDt = shanghaiSdf.parse("1988-07-02 12:00:00");
		Assert.assertEquals(583815600000L, shanghaiDt.getTime());

		Date utcDt = utcSdf.parse("1988-07-02 12:00:00");
		Assert.assertEquals(583848000000L, utcDt.getTime());

		Assert.assertEquals((long) (9 * 60 * 60 * 1000), utcDt.getTime() - shanghaiDt.getTime());// 9H
	}

	/**
	 * 测试闰秒
	 * 
	 * @throws ParseException
	 */
	@Test
	public void testLeapSeconds() throws ParseException {
		Date utcDt1 = utcSdf.parse("2008-12-31 23:59:59");
		Assert.assertEquals(1230767999000L, utcDt1.getTime());
		Date utcDt2 = utcSdf.parse("2008-12-31 23:59:60");
		Assert.assertEquals(1230768000000L, utcDt2.getTime());// +1s
		Date utcDt3 = utcSdf.parse("2009-01-01 00:00:00");
		Assert.assertEquals(1230768000000L, utcDt3.getTime());// =23:59:60
	}

	/**
	 * 测试本地时间调整1,没有异常
	 * 
	 * @throws ParseException
	 */
	@Test // 本地时间,时间调整
	public void testLocalTime() throws ParseException {
		Date shanghaiDt = shanghaiSdf.parse("1927-12-31 23:54:07");
		Assert.assertEquals(-1325491553000L, shanghaiDt.getTime());

		Date shanghaiDt2 = shanghaiSdf.parse("1927-12-31 23:54:08");
		Assert.assertEquals(-1325491552000L, shanghaiDt2.getTime());

		Assert.assertEquals((long) (1 * 1000), shanghaiDt2.getTime() - shanghaiDt.getTime());// 1S
	}

	/**
	 * 测试本地时间调整2
	 * 
	 * @throws ParseException
	 */
	@Test
	public void test6() throws ParseException {

		Date shanghaiDt = shanghaiSdf.parse("1900-01-01 12:00:00");
		Date utcDt = utcSdf.parse("1900-01-01 12:00:00");
		Assert.assertEquals((long) ((8 * 60 * 60 + 5 * 60 + 43) * 1000), utcDt.getTime() - shanghaiDt.getTime());// 8:05:43

		Date shanghaiDt2 = shanghaiSdf.parse("1901-01-01 12:00:00");
		Date utcDt2 = utcSdf.parse("1901-01-01 12:00:00");
		Assert.assertEquals((long) (8 * 60 * 60 * 1000), utcDt2.getTime() - shanghaiDt2.getTime());// 8H

		Date shanghaiDt3 = shanghaiSdf.parse("1900-12-31 23:54:16");
		Date utcDt3 = utcSdf.parse("1900-12-31 23:54:16");
		Assert.assertEquals((long) ((8 * 60 * 60 + 5 * 60 + 43) * 1000), utcDt3.getTime() - shanghaiDt3.getTime());// 8:05:43

		Date shanghaiDt4 = shanghaiSdf.parse("1900-12-31 23:54:17");
		Date utcDt4 = utcSdf.parse("1900-12-31 23:54:17");
		Assert.assertEquals((long) (8 * 60 * 60 * 1000), utcDt4.getTime() - shanghaiDt4.getTime());// 8H
		
		Assert.assertEquals((long) (( 5 * 60 + 44)* 1000), shanghaiDt4.getTime() - shanghaiDt3.getTime());// 00:05:43
	}
}

参考资料

  1. How Does an Atomic Clock Work?
  2. Working with time zones in Ruby on Rails
  3. 你确信你了解时间吗?
  4. 时间频率和铯原子钟
  5. 关于闰秒
  6. 元旦我国第27次闰秒将来临:多一秒世界有何不同
  7. 关于Oracle Timezone的一点总结
  8. ISO8601 Dates in Ruby

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Eclipse Maven依赖缺失问题解决

这段时间一直很忙,项目还有生活上的事,期间也看了一些书,主要是网络连接方面的,对TCP处理以及整个网络的架构有了一定的了解。但是简单的总结感觉意义不大,后面如果遇到问题再具体分析写下总结。此次写一个自己近期遇到的小问题,大体如下:

上周我们组的同事反映海外收付款工程编译报错,很多maven依赖的jar包在eclipse里面没有了,包括最常用的commons-lang3包,还有很多其他包,缺失的包也没什么明显特征。这个问题比较突然,一直以来都没问题,其他境内的工程也没问题。我们仔细想了一下,近期也没改什么东西,除了把这个海外收付款开发流重新复制一个之外(不过问题应该和这个无关)。

我尝试运行了mvn dependency:tree显示能看到commons-lang3包,其他包也有。那这个依赖关系和正常工程比对了一下,有个别公共基础包版本不一致,但缺失的包版本都是一致的。

为了进一步缩小问题的排查点,我决定将出错工程的parent pom文件改成正常工程的样子,这时发现还是报错,而且还出现了新的错误,有一个包由于版本不对,导致编译错误。看来问题不在这,我又仔细看了maven的build日志,发现个问题,日志中download的公共包的pom文件版本不太对,有些是3.3-hw版,有些是3.3.11-hw版本(maven有一个警告,版本号中包含表达式,不是常量)。看来很可能是版本的问题。

又进一步进行了排查,此海外工程的版本为3.3.11且parent pom文件版本也是3.3.11;不一致的3.3版本是因为海外工程显示声明依赖一个vo包,这个包的版本是3.3,且这个vo包的parent pom文件的版本也是3.3,但这个parent pom文件和海外工程的parent pom名称相同。也就是说海外工程同时依赖了两个不同的parent pom文件,所以日志中才会出现两个不同的版本号。具体如下图所示:

异常依赖示意图

问了下配置管理员,是因为当时图省事,没有对vo重新打包新版本。这个肯定有问题,我们把vo包重新打了新版本,海外出错工程的pom文件也修改正确。然后发现eclipse中maven缺失的依赖回来了,问题解决了 ^_^

看来这个是由于maven版本依赖冲突导致的问题,后来有研究了一些maven的资料1 2。发现其实maven有一套自己的版本冲突解决策略,所以在执行mvn dependency:tree时可以看到基本正确的依赖树,一般程序也没问题。但版本冲突总是有害的,那怎么才能显示出所有的冲突的依赖呢?

可以执行下面的命令:mvn dependency:tree -Dverbose,增加了这个选项,maven会显示出完整的依赖包,且会标出哪些是重复的依赖(omitted for duplicate),哪些是冲突的(omitted for conflict)。对于冲突的可以手工解决,例如使用exclude选项。

其实我感觉此次依赖缺失的问题更像是eclipse 或是maven插件的bug,不能正确处理依赖冲突。大家如果后面遇到这种问题也可以看看是不是maven版本依赖冲突导致的。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》