一个简单功能背后需要付出的大量工作

自从我于2021年4月创办这个网站后,我就猜到用户会上传受密码保护的PDF文件。我想如果要做支持这类PDF的功能太麻烦了,所以我推迟了这个功能的研发。最近一个用户请求我,让网站支持密码保护的PDF,所以我就完成了这个功能。 这是一个简单的功能,跑起来也没什么问题。我把昨天所有的时间都花在了它身上,并在今天凌晨2:32把它推送进生产环境。从我过去几天收集的少量数据来看,大约有5%的用户上传带有密码的PDF文件。在这以前,我通常会拒绝用户上传,甚至没有显示错误消息。这样就流失了很多这样的用户,他们大概会认为这个网站不能用。 我花了很长时间才完成这个功能,这让我惊讶的同时也很恼火,所以我想与您分享这段经历,这样您就可以感谢我为您所做的很多工作。[^1] 星期六 晚上11:46 - 向file_mapping表添加字段 file_mapping表存储关于用户上传的每个PDF的信息。我添加了两个字段password和requires_password。我的想法是将密码与其他文档元数据一起存储在数据库中。 周日 上午11:10 - 当API收到一个受保护的PDF时 我编辑了API,以便当它创建一个file_mapping记录时,requires_password设置为true,而pdf_type设置为UNKNOWN.在之前pdf_type可能是基于文字或基于图像。由于我们不能在没有密码的情况下读取文件,所以我们不能分析它是基于文本的还是基于图像的。 我们这样做是因为,文本PDF和图像PDF使用的代码路径不同 周日下午 1:14 - 编写SQL,更新file_mapping记录 UPDATE file_mapping SET password = ? WHERE uuid = ?; 周日下午 2:55 - 更新了SQL语句并创建了一个API来设置文档的密码 UPDATE file_mapping SET password = ? pdf_type = ? page_count = ? WHERE uuid = ?; 当我编写API时,我意识到在用户成功为文档设置密码后,我希望在file_mapping表中修改更多字段。一旦读取了文档,就可以对其pdf_type进行分类并计算文档的页数。 val fileMappings = repository.getFileMappings(body.passwords.map { it.uuid }) val updates = mutableMapOf<String, UpdateFileMapping>() val results = fileMappings.map { mapping -> mapping.

Read more →

PDF 银行对账单 JSON API

1. 获取授权令牌 在 Google Chrome 打开 https://bankstatementconverter.com/ 登录 点击F12打开开发工具 单击“应用程序”选项卡 单击左边面板上的“存储” -> “本地存储” -> “https://bankstatementconverter.com/” 您应该可以看到一个名为“BSC-AuthToken”的键和相应的值。 该值即为授权令牌,复制该值。 2.上传PDF API Method: POST URL: https://api2.bankstatementconverter.com/api/v1/BankStatement Headers: { Authorization: AUTH_TOKEN } Body: Multipart Form Data 请求头请求主体响应主体3.转换PDF API Method: POST URL: https://api2.bankstatementconverter.com/api/v1/BankStatement/convert?format=JSON Headers: { Authorization: AUTH_TOKEN } Body: A list of UUID strings in JSON. The UUID comes from the response of the Upload PDF API. 请求头请求主体响应主体 您将获得一个存储有文档转换结果的数组,其中每个UUID对应您在请求主体中提供的UUID。每个结果都提供一个原始的和一个规范化过的属性。原始属性提供转账信息中的所有列。规范化的属性只包含日期、备注和金额列。正如您在下面的示例中所看到的,借方金额值“5.00”在规范化时变成了“-5.00”。 [{ "raw": [ { "date": "03/08/20", "description": "Monthly Service Fee", "credit": null, "debit": "5.

Read more →

打地鼠型开发——汇丰银行

上周,有一位客户请我帮他处理几百份文件。他有几个银行账户的PDF文件,最早可追溯到2018年。我看了一个文件,给出了一个报价,然后开始处理他的文件。我的想法是把文件塞给银行对账单转换器(BSC)的PDF转CSV处理器。然而,有一些错误出现了。BSC有一个通用算法来检测PDF文件中的交易记录。这通常能完成任务,对许多银行来说,它工作得很完美。不过,仍有很多对账单无法适用通用算法。为了解决这个问题,我会检测文档类型,然后为该文档运行自定义代码。 客户通常会报告某种解析错误。我向他们寻要导致解析问题的文档。然后我将为该文档类型创建一个自定义解析器。我认为这种过程被称为“打地鼠”开发方式。在这篇博客文章,我为汇丰银行对账单创建了一个漂亮的自定义处理器,我觉得带您走一遍我的开发流程可能会很有趣。 目前是什么样子? 看起来很烂!备注的高度参差不齐。由于某种原因,一个日期值不见了。日期上还没有年份。结果被拆分为两个表。缺少货币值。让我们仔细研究一下每一个问题,然后想出一些解决办法。 备注的高度参差不齐 这个挺好对付的。当我合并跨多行的备注时,我会添加一个新的行字符。然而,我认为添加空格字符会更好。没有人想在他们的CSV中出现高低不平的行高度。我正在考虑对所有文件都这么做,而不仅仅是汇丰的这份。 由于某种原因,一个日期值不见了 这是汇丰银行对账单的一个特点。如果一天发生了多次转账,他们只打印第一次的日期。这确实有点烦人。也并没有使结账单更便于阅读。这也让我——PDF小王子的生活变得更加艰难。这也不是太难解决,如果日期是空白的,我可以从以前的记录复制过来。 结果被拆分为两个表 这是因为结单中包括我的港元储蓄户口,以及我的美元及澳元外币户口。外币帐户有一个额外的“CCY”栏。通用算法足够聪明,它可以将具有相同表头的表合并在一起。如果表头不同,则并排输出表。 我们可以通过在我的港元储蓄记录中添加“CCY”一栏来解决这个问题。 缺少货币值 解决方案同缺少的日期值问题。 日期值没有年份 我们可以使用我在之前的博客文章中开发的新技术来确定报表日期的年份。我们开始编写代码吧。 备注的高度参差不齐 我更改了rowmerger.kt,让它接受被分隔的字符串。我再传入了一个空格字符。挺简单的。 缺少日期和货币值 我在之前的文档中已经处理过这个问题。通过调用这两个函数,我们解决了这个问题 结果被拆分为两个表。 我对我的解决方案不是很满意。我基本上转换任何没有“CCY”列的TableHeaders。这使得的TableHeaders在整个文档中是相同的,这意味着它们可以很好地拼接在一起。这肯定是一个奇淫技巧。如果使用gridTransform方法,效果可能会更好 日期值没有年份 解决这个问题需要两个步骤。首先,我编写了一些代码来捕获语句日期。 然后,我通过调用EnrickDates来转换所有在日期列中的日期值。我在之前的博客文章中有讨论过一点日期代码. 总结 看起来好多了。然而,你仍可以说它不完美。CCY列中有一些空白值。如果能将这些设置为港币那会更好。还有,在余额列中也有会出现空当。这是因为汇丰的报表每天只显示一次余额。我们可以通过计算得出这个值。不过,我认为这是自找麻烦。

今年是哪一年?

最近我一直收到很多投诉,说银行对账单转换器导出的CSV结果年份是错的。我最初收到这些投诉时,我很疑惑:“你在说什么?程序所做的只是找到交易数据,将其写入CSV文件并输出。年份怎么会错呢?”。我们就以我的一份汇丰银行对账单为例,看看这到底是怎么回事。 转换后如下图所示 下载后,在文本编辑器(如Sublime Text)中打开,它看起来是这样的。 Date,Transaction Details,Deposit,Withdrawal,Balance 20 Feb,B/F BALANCE,,,"XXX" 27 Feb,CREDIT INTEREST,3.84,,"XXX" 如果在Excel中打开它,它看起来是这样 看一下图像顶部的日期。它的年份设定为2022年,但在CSV中却没有显示年份。Excel是一个自作聪明的混蛋,它自说自话自己添加了今年年份。 这是我们的问题吗? 我可以对我的客户说:“好吧,实际上是Excel它自作主张添加的年份。我们生成的CSV是正确的,这是Excel的问题“。我很想这么说,但我们一定有办法找出银行对账单的正确年份。让我们看看我的汇丰银行对账单的顶部。 这里有一个神秘的无标记日期,上面写着“20 March 2021”。我们不知道它是什么的日期,我们就暂且假设这个日期是“结单日期”或对账单生成的日期。让我们看看我们能否研究出一种方法,向我们目前所有的日期添加正确的年份 例 #1 - 三月 输入 Statement Date: 20 March 2021 Dates: 20 Feb, 27 Feb, 2 Mar, 4 Mar, 9 Mar, 12 Mar, 16 Mar 输出 20 Feb 2021 27 Feb 2021 2 Mar 2021 4 Mar 2021 9 Mar 2021 12 Mar 2021 16 Mar 2021 看上去我们能很快地解决这个问题。我们从结单日期中获取年份,并将其附加到日期中。 例 #2 - 一月 输入 Statement Date: 20 January 2021

Read more →

自动获取您的银行交易信息

几天前,我有了一些关于银行对账单转换器能解决什么问题的启示。最显而易见的答案似乎是“它解决了从PDF银行对账单中提取交易数据难的问题”。这个回答没错,但你也可以说它解决了一个更为普遍的问题。“它方便了用户访问他们的银行交易数据”。但是,如果使用目的是这个的话,那用起来确实有点痛苦。如果您想要要获得2021年的交易数据,您需要: 登录网上银行 从每个银行帐户下载12份PDF文件 登陆 bankstatementconverter.com 上传12个文件 单击转换 如果我们能删掉一些步骤,不会很棒吗?我一直在想办法解决这个问题。 方法 #1 - 主动型浏览器扩展 也许我们可以创建一个浏览器扩展,它可以控制您的web浏览器,并自动下载您的PDF银行对账单。用户将执行如下操作: 登录他们的网上银行 告诉浏览器扩展自动下载文档 浏览器扩展将点击链接,按下按钮并填写字段,获得所有PDF文档 这个解决方案的一个好处是,用户不需要给我们他们的网上银行凭证。第三方也可以对扩展进行审计,以证明它没有做任何恶意的事情。 不利的一面是,这需要为每个网上银行平台建立扩展插件。没有一个“通用解决方案”。 我很难受,我不认为这在技术上有可行性。我还没有证实这一点,但我高度怀疑网络浏览器不会让第三方扩展“主动控制”浏览器。 方法 #2 - 引导型浏览器扩展 这种方法与前面的方法非常相似,只不过这次浏览器扩展只是告诉用户要做什么。“点击这里”,“填写XYZ”,“按此按钮”。效果不太好,但还算是有点帮助。 这个插件应该是可以实现的,但用户是否会觉得这有帮助? 方法 #3 - 主动型桌面应用程序 此方法类似于方法 #1,不同的是它将是一个Windows/MacOS应用程序,而不是一个浏览器扩展。如果浏览器扩展不能在页面中执行命令,那么桌面应用程序可能可以控制用户鼠标/键盘。这个程序很难写,我不确定它到底应该如何工作,但我觉得这是可行的。这种方法的一个好处是,用户不需要给我们他们的凭据,一切都发生在用户的计算机上,银行将无法屏蔽这个应用程序。 方法 #4 - 全自动API 这个方法涉及创建一个API,登录到您的网上银行,下载您的PDF报表,然后处理它们。以下是我想象的工作方式: 用户向我们提供他们的网上银行凭据 我们的服务器调用他们银行的API并下载他们的PDF银行对账单 我们的服务器将他们的银行对账单从PDF转换为CSV 我们将CSV结果提供给用户 这种方法的一个好处是用户只需做很少的事情。不过也有不少缺点!出于某些原因,银行不喜欢第三方从他们的网上银行门户中提取数据,他们设置了很多障碍来防止这种行为。与这些障碍作斗争需要大量的工作。我们需要进行不断地监控,以侦查我们的插件何时会被银行屏蔽。 如何构建所有的插件? 我有两个银行账户。这意味着我可以亲自编写代码从我的两个银行账户中提取数据。可悲的是(如果想让转换器使用这个插件),世界上可不止只有两家银行。如果我在一个银行没有账户,我怎么创建插件。以下是几种方法: 付钱给开发人员,让他们为他们的银行设计插件 创建一个工具,使非开发人员用户可以为他们自己的银行创建插件 发疯的想法,在世界上每一家银行都开一个银行账户。 付钱,让人们允许我访问他们的银行账户,这样我就可以为它建立一个插件 这是一个挺头大的问题,我不知道如何处理它。但这个问题目前为止不大重要。我有两个银行账户,我可以先为它们建立插件。

在十个月内,让月度经常性收入达到1000美元

几周前,BankStatementConverter(BSC)的每月的经常性收入(MRR)超过1000美元。我猜你们会有兴趣听我聊聊这段经历 2021年3月至7月,MRR为0美元 我产生了构建BSC来帮助用户处理PDF的想法。我花了大约一个星期在Kotlin摸索,看看我的想法是否可行,它确实是可行的。不久之后,我和一个朋友喝啤酒时告诉了他我的想法。“请不要做这个项目,我之前有接触过PDF,这玩意弄起来就像噩梦”。 这倒引起了我的兴趣,我心的一部分想做这个项目,无论如何,我想证明他是错的,但另一部分的我意识到,如果这个问题如此困难,那么解决方案很可能不存在。我脑子里有一个非常清晰的逻辑,我还有另外一个朋友想和我一起建造这个项目。我和这位朋友去喝了杯啤酒,同时向他推销这个主意。 他喜欢这个想法,我们同意一起建造它,他做前端,我做后端。我们两个合作得很好,我们在2021年4月4日见面,开始开发这个软件,几天后网站就上线了。在4月17日,我们购买了一些谷歌搜索广告,获得了第一批用户。 在5月6日,我添加了代码,这样用户就可以购买点数用以处理PDF。七天过去了,没有人购买任何点数。我开始想“嗯….也许这个应用程序还是失败了”。我放了一天假,走了六个小时的徒步。就在我准备开始远足的时候,我收到了一个用户寻求支持的电子邮件 “嗨,我买了一些点数,但后来应用程序崩溃了。请帮忙“我快速看了一下,我看到用户购买了43.99美元的点数!于是我疯狂地试图在我的手机上解决他的问题。 第一次卖出成功后不久,我的合作伙伴决定退出,他和我说这个软件有点太无聊了,但我怀疑他是觉得这个应用不会成功。 收入:10个用户248美元 广告支出:858美元 这两个月的销售额是一次性售出的,所以没有经常性收入。花费858美元,带来248美元是一个失败的投资,但这是我能想到的唯一吸引用户的方法。 2021年7月至10月,200美元MRR 我决定从销售PDF页点数转向每季度20美元的订阅。这会有更好的转化率。我还对我的处理器进行了各种改进,它能处理扫描的PDF和非银行对账单PDF。 收入:20个用户740美元 广告支出:3050美元 2021年10月至2022年2月23日,1106美元MRR, 进入10月份,我感觉非常失望,我的应用程序难以获得新用户,每天的广告支出导致资金亏损。我决定放弃这个项目并关掉了广告。这时,出于某种原因,一位家庭朋友约我一起喝咖啡。她问我在做什么,我告诉她关于应用程序的事情,她完全不为所动,并说服我继续回到银行工作。 “你回到银行难道不是更开心吗?” 这反倒激励了我,我想证明她是错的。在和她见面之前,我一直有在认真考虑回去工作。在这段时期,我遇到了这篇文章. 它说服了我,我开始写博客文章。我还提高了订阅价格,并增加了每月和每年的订阅计划。博客帖子立即开始带来流量,我也走运了一回,我的一篇博客文章登上了HackerNews的首页。从HackerNews来的人并没有注册,但进入首页给我带来了很多外链,并提高了我的谷歌排名。 从这里,我了解到内容营销是一个比付费点击更有效的营销策略。一个好的内容可以带来多年的流量,而一旦你停止付广告费,就不会产生流量。 更简单地说,我要发布广告的话便不能赚钱。很明显,对我来说,我必须停止投放广告。 收入:4272美元 广告支出:$0 结论 我暂且不会说我成功了,这个应用程序暂时还不能支付我的生活费,但我认为它最终会成功的。这是我第一次构建SaaS应用程序,我学到了很多。我简单地谈了一下整个过程,在这个过程中最难的是孤独,我真的很喜欢为这个应用程序编写代码,但我非常想念和其他人一起工作。

检测转账表的表头

这是系列博客文章中的第二部分,在本系列中,我解释了银行对账单转换器是如何工作的。在上一篇文章我谈到了如何从PDF中提取每个字符及其边界框。在本文中,我将讨论如何使用字符和边界框来检测转账表的头。 val pageRegion = Rectangle(0f, 0f, page.cropBox.width, page.cropBox.height) val lines = LineExtractor(page).extractLines() val headers = headerDetector.detect(allCharacters, lines, pageRegion).map(::headerTransformer) 在上面的代码中,我们确定了页面的大小,从页面中提取图形线,并调用TransactionHeaderDetector类中的detect方法。通常情况下,使用图形线可以用来提高表头的质量,我们将在后面更详细地讨论这一点。 override fun detect(__unsortedCharacters: List<CharAndBound>, graphicalLines: List<Line>, pageRegion: Rectangle): List<TableHeader> { val words = merger.merge(__unsortedCharacters) ... } 首先,我们将字符列合并为单词列。我们通过按字符的Y和X位置对字符进行排序,然后测量字符之间的水平距离和垂直距离来实现这一目的。如果其中某个值超过阈值,我们便认为这是一个新词的开始。下面是一段有趣的代码,CharacterMerger.kt它负责检测单词之间的边界。 val xDistance = current.bound.left() - previous.bound.right() val yDistance = Math.abs(current.bound.bottom() - previous.bound.bottom()) val isCloseEnoughOnX = xDistance <= xThreshold val isCloseEnoughOnY = yDistance <= yThreshold if (!isCloseEnoughOnY || !isCloseEnoughOnX) { words.add(createTextAndBound(buffer)) buffer.clear() } 在此之后,我们将这些单词组成行,并检查每一行,对比下看看他是否对的上银行对账单交易表的标题。如果找到了更多的头,返回headersFound。如果没有找到,就尝试跨行查找头。如果那能够找到,那便太好了!如果不能,我们就一次跨三行查找头。这不是最优雅的解决方案,但在实践中非常有效。在性能方面它不是很友好,在detectLines方法中有许多不必要的重复。 override fun detect(__unsortedCharacters: List<CharAndBound>, graphicalLines: List<Line>, pageRegion: Rectangle): List<TableHeader> { val textLines = splitByYLevel(words) var headersFound = detectLines(textLines, graphicalLines, pageRegion, 1) if (headersFound.

Read more →

从PDF中提取文本和边界框

这是我解释BankStatementConverter如何工作系列博客文章的第一部分。在这篇文章中,我将解释获取边界框和其他字符属性的代码。许多代码都是基于DrawPrintTextLocations和PDFTextstripper. 我们要做的第一件事就是使用PDFBox加载PDF文件,然后我们再逐页处理文档。我们需要逐页处理PDF,避免内存耗尽,大多数文档不到十页长,但也有超过10,000页长的文档,如果我们试图将一个大文档中的所有数据加载到内存中,我们会很快耗尽内存,我们的应用程序也会奔溃。我们使用parsePage方法提取页面上的所有字符,我们会得到一个CharAndBound对象列表。 data class CharAndBound( val char: Char, val bound: Rectangle, val color: Int, val fontCode: Int, val rotation: Int ) : IRectangle by bound FastCharacterParser FastCharacterParser类的stripPage方法负责返回CharAndBound对象列表。它继承了pdfbox的PDFStreamEngine类并侦听以下事件: private fun setOperators() { addOperator(ShowText()) addOperator(BeginText()) addOperator(Concatenate()) addOperator(DrawObject()) addOperator(Save()) addOperator(Restore()) addOperator(NextLine()) addOperator(MoveText()) addOperator(MoveTextSetLeading()) addOperator(SetFontAndSize()) addOperator(ShowTextAdjusted()) addOperator(SetTextLeading()) addOperator(SetMatrix()) addOperator(ShowTextLine()) } 当PDFStreamEngine遇到字符,它将调用FastCharacterParser中的showGlyph方法,在这里我们会计算CharAndBound对象的所有属性。 override fun showGlyph( textMatrix: Matrix, font: PDFont, code: Int, displacement: Vector ) 我们可以使用textMatrix参数来确定字符在页面上的角度和位置。我们使用字体的边界框来确定字符的宽度和高度。有些字体没有边界框信息,当我们遇到这种情况,我们选择回退到硬编码的边界框,并希望最终结果不会出错。 val bbox = getBoundingBoxWithHackyFallback(font) // 设置步进宽度, bbox 高度 (字符空间) val xAdvance = font.

Read more →

分析我的汇丰银行对账单

我原本想分析我的2014年汇丰银行对账单。对我来说2014年是很棒的一年,我推出了一个新的应用程序,加入了一家游戏开发公司,和一个朋友租了一套公寓。但不幸的是,我的2014年汇丰网上银行对账单无法再获取到了,似乎我只能查到有限年份前的账单。那就让我们来看看我的2015年银行对账单。2015年对我来说是相当艰难的一年,我辞去了游戏开发人员的工作,因为我自制的一款应用销售量非常好。然而我辞职几周后,我的应用程序就从 Google Play 中删除了。在接下来的几个月里,我在开发一个iPhone应用程序。 我为了从我的PDF银行对账单中获取交易数据,制作了 银行对账单转换器 ,用于我公司的年度审计。 在2015年,我还没有公司,所以我从来没有提取甚至看过我的对账单。在这篇文章中,我将介绍如何从汇丰银行获得PDF文件,如何提取交易数据。接着,我们将对所有内容进行分类,并讨论一些有趣的交易。 从汇丰下载PDF报表 登录您的汇丰银行 网上银行门户 转到银行 -> 服务和支付 -> eStatement/eAdvision -> 查看/管理文档。 单击 Select Month 在下拉框中选择年和月。我选的是2015年和1月。 点击 View Result. 您应该能看到在 eStatements / eAdvice 区域有一个文档列出 点击 View 按钮. PDF应该会在另一个选项卡中打开。 在新打开存有PDF的选项卡中,单击 download 按钮. 重复步骤4-7直到你获取了所有你需要的文件。 到目前为止,您应该有了一个装满了PDF文件的文件夹,我的PDF命名规则为年份在前,后跟补充0的月份,这意味着PDF可以按字母顺序排序,也就是从最老的到最新的顺序排序。这种方法一箭双雕 将PDF银行对账单转换为Excel 访问 bankstatmentconverter.com 点击蓝色的大按钮,上面写着 Click here to convert a PDF! 选择您刚刚下载的所有银行对账单。 等待文件上传。您最终会看到如下图所示的内容。 点击 Convert all into one CSV 按钮 应该会跳转到一个新的页面,在那里您可以看到所有交易详细信息。在那里,您可以通过点击 Download 按钮下载所有CSV数据。 分析 我在Excel中打开CSV数据,然后将其复制到Google Sheets文档中。

Read more →

我处理OutofMemoryErrors报错的五种姿势

我使用Grafana创建可视化图表,它能向我展示银行对账单转换器的各种业务和性能指标。其中,我创建了一个图表,它能跟踪服务器返回给客户端的内部服务器错误数量。我是这么写的,每当有一个500报错发送到客户端时,我就向数据库中写入一条记录。这个图表对于解决我没有预料到的bug非常有帮助。上周四香港时间凌晨12:55,我的服务器开始抛出Java臭名昭著的OutofMemoryErrors报错。 { "message":"Java heap space", "errorType":"UNKNOWN", "cause":{ "type":"OutOfMemoryError", "detailMessage":"Java heap space", "stackTrace":[] } } 几个月前,我遇到了一些类似的错误,我通过升级服务器实例内存来解决,从1 Gb升级到了4 GB。那时我在我的服务器上运行tesseract,用以OCR识别基于图像的PDF。Tesseract使用了相当多的内存,所以我觉得我需要升级服务器的内存。我最近用亚马逊的textract替换了tesseract,所以我不再需要额外的内存来处理OCR图像。当我上周看到这些报错时,我想“难道我服务器上的4 GB内存快用完了?4 GB肯定能处理一个PDF文件了”。所以我决定优化我的代码,而不是通过投入更多的硬件来解决这个问题 1. 修复UI问题 当用户上传PDF并按下转换按钮时,UI将移动到 /converted 页面。 在这个页面中,UI将调用一个API,它会尝试自动检测PDF中的转账数据。 如果该API未能找到转账数据,则UI将移动到 /previewPDF 页面。 在此页面中,用户可以选择要提取的区域。但这里有一个小bug,导致出现下图中的状态。 下面的四个转账显示了用户在三秒钟内调用转换API四次。他们为什么要这么做?我花了一段时间才弄明白,但我最后还是弄明白到底发生了什么: 1. 用户上传一个PDF文件 2. 用户点击转换按钮 3. UI 带他们来到了 /converted 页面 4. API说它不能自动查找到转账信息 5. UI 带他们来到了 /previewPDF 页面 6. 用户按下返回按钮, UI 带他们来到 /converted page 7. 返回第#4步 大致上是这样的,用户希望返回到根页面,但UI最终会将他们带回到预览PDF页面。 之前 if (error.errorType === 'FAILED_TO_FIND_TRANSACTIONS') { router.push('/previewPdf?uuids=' + uuid) return } 之后 if (error.errorType === 'FAILED_TO_FIND_TRANSACTIONS') { router.

Read more →

Join The Mailing List