低级错误,跨年的礼物 - 用substring识别跨年

低级错误,跨年的礼物 - 用substring识别跨年

​ 1月4日放完元旦回公司上班,早上正摸鱼呢,哦,不对,正学习呢,看到了公众号推送的文章:12月31日写成13月1日引发重大 Bug,程序员新年就要被“祭天”?。具体是怎么写出的bug那就不得而知了,难道这位自己手动解析时间戳??

​ 还没等跟我同事浩哥吐槽这篇文章呢,跨年的bug来到我们头上了。业务在群里@我们,投诉说一个公文系统的公文编号在新的一年里没有重置从1开始,而是在2020年的基础上继续递增延续……可我记得这个功能是有实现的,代码我还瞄过一眼,尽管代码不是我写的(不是甩锅)。

​ 好吧,看代码排查了,代码不复杂顺着看下来,尽管我一眼就看出问题了,但我只能说我这这这…

1. 业务需求背景

​ 公文系统,每一个业务流程都有一个唯一的业务编号,简化为:简称[年]XXXX号,如:粤[2020]0403号、鄂[2019]0724号。简称为所在省份,方括号中为年份,后面代表该年第几号文件。

​ 业务需求:每年1月1日开始,编号从1开始编号,4位数字,不足补零

2. 功能设计

​ 因为每个省份的编号肯定是不统一的,所以维护了一张省份简称及公文编号表,如下:

序号省份代号省份简称公文编号(已用的最大编号)最后更改时间
10204032016-4-3 12:20:20
20277242017-7-24 15:34:35
30101042018-10-4 16:00:00

​ 代码逻辑:

  1. 生成公文编号时,根据省份代号查询上表,获取省份简称、公文编号、最后更改时间并锁表;

  2. 取当前系统时间,跟最后更改时间作比较。

    如果是同一年,公文编号+1作为要生成的公文编号,更新表中公文编号及最后更改时间;

    如果不是同一年,公文编号置为1,作为要生成的公文编号,更新表中公文编号及最后更改时间

​ 这代码逻辑本身这么设计也ok,问题在于比较是否为同一年的代码上…

3. bug

​ 代码简单复原如下:

/**
 * 获取公文编号
 * @Author Nile(576109623@qq.com)
 * @Date 0:11 2021/1/10
 * @param lastUpdateTime : 最后更新时间
 * @param no : 已使用的公文编号
 * @return : int
 */
public int getOfficialNo(Date lastUpdateTime, int no) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String now = format.format(new Date());
    String last = format.format(lastUpdateTime);
    if (StringUtils.substring(now, 0, 3).equals(StringUtils.substring(last, 0, 3))) {
        // 年份一样,递增,返回
        return ++no;
    } else {
        // 年份不同,返回1
        return 1;
    }
}

​ 看出问题没有!!比较年份是将时间转为字符串,然后取前4位进行比较。ok,这种方式笔者也就不吐槽了,关键是:StringUtils.substring(String str, int start, int end) 这个api用错了!!

​ 传入的两个序号start和end,截取的字符串是不包含end位的,简单说就是包头不包尾,源码如下,注释中也给出了栗子,参数end的解释为(exclusive排除):

/**
 * @param end  the position to end at (exclusive), negative means
 * count back from the end of the String by this many characters
 */

​ 而最让人啼笑皆非的是这个bug去年没有发生,系统是2019年上线。2019跨到2020,对比前三位201和202,不一致,所以公文号重置为1了。今年是2020跨到2021,对比前三位202和202,一致,不重置继续递增

​ 好吧,我服了。

package org.apache.commons.lang;
/**
 * <p>Gets a substring from the specified String avoiding exceptions.</p>
 *
     * <p>A negative start position can be used to start/end <code>n</code>
 * characters from the end of the String.</p>
 *
 * <p>The returned substring starts with the character in the <code>start</code>
 * position and ends before the <code>end</code> position. All position counting is
 * zero-based -- i.e., to start at the beginning of the string use
 * <code>start = 0</code>. Negative start and end positions can be used to
 * specify offsets relative to the end of the String.</p>
 *
 * <p>If <code>start</code> is not strictly to the left of <code>end</code>, ""
 * is returned.</p>
 *
 * <pre>
 * StringUtils.substring(null, *, *)    = null
 * StringUtils.substring("", * ,  *)    = "";
 * StringUtils.substring("abc", 0, 2)   = "ab"
 * StringUtils.substring("abc", 2, 0)   = ""
 * StringUtils.substring("abc", 2, 4)   = "c"
 * StringUtils.substring("abc", 4, 6)   = ""
 * StringUtils.substring("abc", 2, 2)   = ""
 * StringUtils.substring("abc", -2, -1) = "b"
 * StringUtils.substring("abc", -4, 2)  = "ab"
 * </pre>
 *
 * @param str  the String to get the substring from, may be null
 * @param start  the position to start from, negative means
 *  count back from the end of the String by this many characters
 * @param end  the position to end at (exclusive), negative means
 *  count back from the end of the String by this many characters
 * @return substring from start position to end positon,
 *  <code>null</code> if null String input
 */
public static String substring(String str, int start, int end) {
    if (str == null) {
        return null;
    }

    // handle negatives
    if (end < 0) {
    	end = str.length() + end; // remember end is negative
    }
    if (start < 0) {
    	start = str.length() + start; // remember start is negative
    }

    // check length next
    if (end > str.length()) {
    	end = str.length();
    }

    // if start is greater than end, return ""
    if (start > end) {
    	return EMPTY;
    }

    if (start < 0) {
    	start = 0;
    }
    if (end < 0) {
    	end = 0;
    }

    return str.substring(start, end);
    }
}

4. 解决

​ 方案:

  1. 不管,就按这个顺序继续递增,反正一年一个省份的公文数量也不会超过5000,4位数够用两年了。(这么回复业务可以打包走人了)

  2. 更新数据库表,将2中表字段“公文编号”所有省份统一更新为5000,然后大家一起从5001出发,欧耶。(这踏马跟方案1有什么区别)

  3. 统计每一个省份2021年已使用的公文号数量,将表字段“公文编号”置为该数量,这样就不影响系统的继续使用。如:统计广东2021年已发文4篇,将广东的“公文编号”置为4,接下去发文的编号就会是:粤[2021]0005号。但2021年已生成的编号就要更改数据从1开始,不然到了年底就重号了,好在是新年第一天,公文审批还没有结束,全国的发文数也还在可接受范围内。

​ 准备更新脚本、去机房更新数据,3个人半天没了。

​ 修复代码,上线。(2021年底前上线就行,这个好狗)

5. End

​ 其实像这样的api,类似的还有list的切割。

List<Integer> ints = new ArrayList<>();
// 截取第0-999这1000条数据,第二位不要理解为条数,因为第一位不一定是0
List<Integer> list = ints.subList(0,1000);

​ 之前开发的时候笔者也被坑过,好在单元测试发现了。各位共勉、谨记,单元测试记得跑。

​ 我们这边修复完了正吐槽呢,隔壁组项目经理说他们的另一个公文系统忘记设计这个功能了,产品设计时业务也没有提这个需求,哈哈哈哈哈哈,容我笑会。那他们只能按4中的1方案执行了,明年再重置。

​ 另外一个组,说跨年后他们系统的公文号跳号了,就是生成的公文号都是奇数,跨年欢乐多,你们高兴就好。

6. 巨人的肩膀

​ 本来是想写一点关于日期的,但网上已经有很多文章了,就不折腾了,之前收藏的一篇文章:JavaSE基础:扩展Java 8 日期操作,介绍了LocalDate的使用,可以很方便的获取年月日,Date可以转为LocalDate。

Date date = new Date();
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
int year = localDate.getYear();
int month = localDate.getMonthValue();
int day = localDate.getDayOfMonth();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值