Java线上实习总结-校招VIP-校跑跑P2

今年初本人通过浏览boss直聘获得一次java后端开发线上实习经历,公司是一家it教育机构,专注于大学生校招,名为 “校招VIP” ,该公司通过组织线上实习,吸引大学生,以增加其知名度,以便于推广其校招服务产品,因此本人此次实习并非真正的企业实习。虽然如此,本人还是了解和实践了java开发的工作内容(产品会、功能文档、接口文档、代码编写、代码评审),克服了功能设计和代码编写中的挑战,发现了自己技术能力的不足。总之收获颇丰。

项目介绍

大学生有的时候比较“懒惰”,可以花少量的钱让别人帮忙把快递、外卖等送到面前,这种需求越来越强烈,而“校跑跑”,是一款为大学生提供派单、接单服务的app,派单人发布订单,享受足不出舍的代拿服务,接单人选择合适的订单,赚取零花钱。各大高校学生一经注册即可成为派单者发布需求,而成为接单人需要完成接单人认证并缴纳押金,项目设有送单安全保障、接单月榜、钱包等功能,目前该项目处在开发阶段。

“校跑跑”P1阶段早已完成,且该公司已开设了校跑跑P1的项目课程,如下图所示,由于本人没有参与,故感兴趣的读者可以通过微信小程序“校招VIP”查看该阶段项目的详细内容。

本人参加的是P2阶段

1.接单人认证:为了确保接单人必须是本校学生,例如凡是接济南大学的单子的的接单人必须是济南大学学生,一旦通过接单人认证,便可搜索到自己学校的所有订单,即可接单。同时为了避免出现不诚信情况,例如拿而不送,接单人接单前必须交押金。

2.列表多路径:为了便于接单人一趟接多单,挣更多钱而不用多跑路,需要提供一种订单起止点(例如某奶茶订单的起点为奶茶店,终点为派单人宿舍)的搜索策略,使得接单人一趟就能实现点到面、面到面、甚至包括线(让接单人把接单路径沿线的目标也送了)。

3.接单月榜:为了鼓励学生接单,提供接单月榜,包括全国榜和院校榜。

4.钱包模块:为了暂存用户在各种业务中产生的金额,包括接单收入、提前充值的金额等,重点是要实现提现功能。

工作内容

公司负责人组织五种分工,分别是产品、UI设计、前端、后端、测试,每个模块的开发流程为模块发布会->产品人员设计脑图和原型图->产品会->后端人员根据原型图撰写设计文档和接口文档->文档评审->前后端编码->测试->代码评审,本人只负责后端,故重点介绍后端工作内容。

1.产品会:参与公司负责人的模块需求介绍,根据原型图对模块的基本功能提出要求,对于后端开发人员理解业务逻辑非常重要,后端需要提前意识到一些技术难点,调研解决方案。

2.功能文档和接口文档:后端人员需要根据产品原型进行设计。功能文档要设计表结构、明确表所对应的实体、明确各个属性的类型及枚举值,同时要说明亮点和难点,简述解决方案;接口文档要尽可能完全考虑到页面中可能需要的接口,必须明确请求参数和返回数据的格式、不能遗漏任何细节(例如分页)。

3.文档评审:表和接口的设计必须严格遵守实习手册,明显违反公司编码规范的细节是不可接受的。亮点和难点的解决方案不要求是最优的,但必需要与自己的整个功能设计自洽。

4.代码编写:代码是在公司提供的基础项目上编写的,一些基本的接口,如用于id的获取可以直接调用;编写完成后,所有的接口必须经测试验证,只有可以正常运行的代码才可提交。

5.代码评审:所有实习生代码都经过公司负责人评审,除了违反明确规定外,代码的评审较为宽容,凡是功能实现没有问题就通过,在此基础上,负责人额外对代码的可读性提出了要求。

由于是线上实习,人数众多,每次评审将会淘汰部分工作不合格的实习生,在钱包模块,本人由于实力不足难以为继,尽管无法继续提交任务,本人还是参加了后续会议,跟进了所有模块的开发过程。

各模块需求及解决方案

1.接单人认证

接单人认证的核心是以下这张表单,大学生都有自己本校的学生邮箱,故采用学生邮箱验证确认用户的在校生身份。

学校名称不能由用户随意输入,故需要提前建立高校库(额外添加了高校邮箱后缀),当用户输入学校首字时,前段发送请求,根据首字查询学校(例如用户输入“齐”,查出“齐工大”和“齐师范”),这个接口很简单,直接like '齐%' 即可。

入学时间和毕业时间需要校验,非空、毕业时间大于当前时间、开学时间小于毕业时间,当时评审者认同这种方法,但我个人还是觉得缺少什么,希望读者提出意见。

学历的校验很明确,无需多言。

当用户选定学校后,邮箱后缀自动填充,仅需输入邮箱名(应该是自己的学号),然后点击发送验证码,输入后提交认证,即可获得接单人身份。在接单人主页可以看到认证状态,从”去认证“变成了可以”查看认证“。

想要实现邮箱验证功能,需要创建163邮箱,开通IMAP/SMTP服务,公司已经提供好了这个服务,故具体编码流程请感兴趣的读者自己搜索资料。

接单人的认证信息是需要有效期的,目前设定每半年需要重新认证。此外,用户可以修改接单人认证信息,但规定每年只得修改3次。

尽管成为了接单人,想要去接单还要支付押金。

押金的支付和提取属于支付服务,不属于本模块的任务,因此只需要建立保存押金信息的表结构,本人一开始将接单人认证信息(学校、邮箱等)和押金信息分别保存在两张不同的表中,但在代码评审会上,用一张名为takerDetail的表保存接单人的所有信息即可。

本模块的开发比较简单,只要理解了产品原型图,文档的撰写就没有问题,在代码方面需要严格遵守实习手册,对用户提交的认证信息尽可能地校验完善(在controller层),即可过关。

可能有人会提出每个学校有多个“校区”的概念,通过某学校认证的接单人可以去所有校区接单呢?还是应该把“学校认证”改为“校区认证”?这个问题应该由负责产品的同学解决,java实习生只管看产品原型写代码就行了,而产品设计实习生考虑的就多了。

2.列表多路径

假如小明在学校附近的奶茶店购买了最流行的紫衣甘蓝,正想回宿舍,突然意识到他可以接几单赚回奶茶钱,于是他打开”校跑跑“,通过地址筛选到了两个从该奶茶店到自己宿舍的订单,可每单只有可怜的2元钱跑腿费,不够自己花费的9元,于是他继续筛选,查找有没有到自己宿舍附近的订单,然而没有,于是他检索自己附近的肯德基有无合适的订单······

然而,并不是所用用户都有小明这样的耐心,因此我们需要在用户输入取货地点和送达地点后,将取货地点和送达地点附近的点位都查询出来,作为订单的查询条件,以便于用户一趟送多单。需要开发“面到面”、“面到点”,“点到面”跑腿时可选择地址的查询功能,在地址筛选时,如果用户选择”附近“,则为一个面,若选择”只本地址“,则是一个点。

在app运营前,学校内部的核心地点是要提前录入,负责产品设计的人员规定,学校数量上限500,每个学校初始化地点数据上限100,每个地点附近100米内的地点称为”附近地点“。

计算每个地点附近的所有地点是一个难点,我采用的是通过保存每个地点的经纬度来计算每两个地点的距离,因此地点表需要经纬度两个字段,想要计算每个地点附近的所有地点,就需要遍历本学校其他所有地点,至多算99次距离,如果用户每次输入地点后才进行计算,势必导致卡顿,因此每个地点的附近地点需要提前计算和保存。

附近所有地点的计算可以在app运营前学校内部的核心地点录入location表后进行,保存在名为location_nearby的表中。

location表,保存地点信息:

location_nearby表,保存每个地点附近的所有地点,以下是评审会后官方版的表,是1-n关系表:

但是我自己的设计是1-1的关系表,额外增加了距离信息,意图来实现一个”优先级“,以距离排序,让用户选择离自己最近的地点,进一步减少路程:

这个表设计使得查询复杂一些,还被其他实习生指出有”数据爆炸“风险。因为一个学校最多100个地点,这样至多有100*99条数据,算不算”数据爆炸“呢?

要注意的是,学校内部的地点不是一成不变的,需要有新增、删除、修改地点的接口,由于修改地点后,location_nearby表也要同时更新,因此这几个接口的实现比较复杂:

1.新增地点a:遍历原有地点,找到新地点a附近的所有地点,设为集合A,在location_nearby表中更新A的nearby_location_ids,同时新增a的nearby_location_ids,最后将新地点加入location表。

新增地点的经纬度必须在学校的经纬度范围内,这个范围保存在学校的范围表中,用四个字段表示:

topLeftX decimal(9, 6) null, topLeftY decimal(9, 6) null, bottomRightX decimal(9, 6) null, bottomRightY decimal(9, 6) null,

2.删除地点b:删除location和location_nearby表中代表b的元组,再遍历location_nearby表,凡是nearby_location_ids中出现b的id,都将bid剔除。

3.修改地点:删除+新增

此外,产品还提出了地图选位、手机定位功能(为了考验前端实习生),其中手机定位后前端只返回经纬度坐标,经纬度变化莫测,无论是mysql还是redis都无法提前保存,因此只能在用户定位后,遍历校内所有地点,算出附近100m所有地点,返回给前端。

以上是列表多路径模块的基本实现思路,我还使用了redis缓存各个学校的常用信息,如校内所有地点、每个地点附近的点位,进一步提高了接口的响应速度。

3.接单月榜

一个定时(30分钟)刷新的全国榜,同时用户可以手动刷新,当用户进入展示页面时,要显示剩余刷新时间,呈现出倒计时效果。同时需要分页,使得全国的接单人的都可以查到。

另一个实时的院校榜,只展示某院校前十名,无分页。

一提到榜单大家一定会想到使用redis的zset,zset的每个成员都关联了一个分数(score),这个分数用于对成员进行排序,在本项目中,以接单人当前的接单总收入为score。实现方案似乎很简单,全国榜单使用一个zset维护全国接单人的当前接单量及总金额,院校榜需要使用多个zset,每个学校使用一个维护本校所有接单人的当前接单信息,这么看来,无论全国榜和院校榜都是实时性的,可是产品原型中为什么全国榜使用定时刷新呢?

我想可能产品设计者认为全国榜数据规模太大导致性能问题,既然定时刷新榜单,那么直接使用定时任务扫描mysql中保存当前接单人接单信息的表,将排序结果缓存入redis,凡是没有到达下次刷新时间的请求,直接返回redis中的榜单数据。至于要返回的剩余刷新时间,只需在redis保存一个时间记录上次扫描mysql的时间,当前端来请求时,计算出剩余刷新时间。如果是前端请求是用户手动刷新,那么扫描mysql,更新redis的榜单信息和刷新时间即可。

4.钱包模块

该模块重点在于余额管理、账单管理,难点是提现接口的实现。

账单分为两大类:收入、支出,收入分为接单收入、退款(如发单未完成);支出分为提现(从钱包到个人支付宝)、支付押金、派单。

余额表的设计要充分考虑到账单有“处理中”这一状态,因此还需要有额外字段保存“已冻结”金额,然而本人在设计时却忽略了这个字段。

接下来是用户提现的逻辑,金额从钱包流入用户指定的支付宝账号,此过程是此次实习最难的一项任务,需要保证提现的安全。

图中的提现流程有两方面安全性问题:

1.用户多次请求提现,提现接口重复执行,导致给用户支付宝多次汇款。

2.提现接口调用支付宝接口后,若无法确定转账的结果,用户余额、账单的状态得不到更新,产生不一致性问题。

问题一是个幂等性问题,决绝方案有多种,这里我将介绍其他实习生的一种解决方法:使用userId等参数构造一个唯一的token,调用redis提供的setNX方法,相同的token只能调用一次,一旦重复调用,提现接口返回错误提示,不再执行。

问题二可以称为一个分布式事务问题,在提现接口中,需要提前冻结部分金额,并生成账单,以避免在提现完成前使用将要提出的金额,然后同步调用第三方接口,在正常情况下,该接口会返回支付结果(成功或者失败),然而会有多种因素(网络问题、接口执行异常)导致无法确定支付结果,而@Transactional无法处理第三方接口,最终导致金额始终冻结、账单始终为“处理中”状态,提现的结果与用户钱包状态不⼀致。

通过查看支付宝文档(链接:转账到支付宝账户 | 支付宝开放平台),可以查到我们需要用到的“单笔转账接口”,其中的业务错误码证实了上述问题的存在,并且提示我们可以通过调用“转账业务单据查询接口”确认支付结果。

直接在转账接口中处理是不合理的,我们无法完全考虑各种不确定情况,因此使用定时任务是个非常好的方案,可以定期搜索处于“处理中”状态的账单,调用支付宝提供的 “转账业务单据查询接口” 确认支付结果,进而更新账单和余额状态。

此外,其他实习生提出使用支付宝转账接口的的异步调用方法,该方法执行完毕后会主动发送消息,需要再写一个接口接收支付是否成功的消息。而评审官提出如果消息接受成功,但接口中对消息的处理发生异常,这又该怎么办?此问题小编实在没有脑力思考了,所以留给读者解决。

本人在该模块的文档评审会后未能通过,提交文档的整体思路没有问题,但没有考虑到幂等性,也没有详细查看支付宝文档。会后,我主动查阅相关资料,了解了“幂等”的概念,并参考了支付宝的相关文档,最终梳理出了上述解决提现安全性问题的方案。因此,建议大家在遇到难题或不理解的问题时,积极查阅资料,这是一个高效且有价值的学习过程。

总结

以上是我在校跑跑P2项目中担任Java开发实习生期间,在模块设计与开发过程中的一些整体思路梳理。虽然文中未详细展开诸如接口设计、参数校验、返回值规范等细节问题,但这些内容在实际开发中至关重要,不容忽视。在钱包模块的评审过程中,评审官最终以“接口文档参数设计错误”为理由否定了我的方案。这也让我深刻认识到:在技术实现中,解决复杂问题固然重要,但对基础细节的把控同样关键。因此,希望大家在今后的学习和工作中,能够重视每一个看似简单的细节——它们往往决定了项目的成败。