产品开发经验总结-让你少奋斗一年的经验之谈
Posted lixinghua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了产品开发经验总结-让你少奋斗一年的经验之谈相关的知识,希望对你有一定的参考价值。
新产品开发历时1年多,总算马马虎虎上线试用1个多月了,目前用户量大概300号左右,租户大概10家左右。这里提到一个“新“字,在我没来到这家公司之前其实已经有自己研发的产品(物流管理系统)在使用了,为什么还要推翻老的设计架构重新设计呢,总结主要有以下几点:
- 数据不准确(如 库存、结算数据等等,数据不准确时还找不到原因,需要手动执行sql直接修改数据库数据。)
- 维护成本太大(由于开发管理不当、需求不清晰、沟通不充分。导致项目混乱,项目表结构都很混乱,业务逻辑全是存储过程,动不动上千行代码的存储过程。)
- 速度慢(大量数据本地化处理,本地过多的查询,过多的不必要查询。)
- 用户体验差(用户不会用,功能隐蔽,反人类设计)
- 安装成本高(给用户装软件的流程是:准备软件->安装软件->调整参数->培训客户 一圈下来好多细节步骤,占据大量时间)
- 用户随便点点报错(开发没考虑好逻辑或偷懒没想好一些意外情况,如 数据为null、超出索引等等)
- 数据安全问题(如何做到保证用户数据的安全)
说实话要解决这些问题难度还是相当大的,当时在公司我能说只有我一个开发。对你没听错就我一个,据说以前的开发经理跑路了~~~哈哈哈。但是作为开发拿到这样的项目,不对,更贴切的叫法应该叫产品。应该是一个难得的机会,怎么说也得上手干一场。刚开始对公司的产品的业务非常不熟悉,许经理给我们进行了大量的业务培训。纵观系统其实很简单就是几条业务线:业务、财务、回单等等。横向技术点有:功能授权、数据授权。其实这里仅仅是表象,内部可不止这么简单。
下面我简单介绍下物流系统中的几大角色(可以简单想成在淘宝购物的物流流程):发货人、收货人、物流公司(含网点、部门、 司机、用户 )、承运公司。这里一些名词还是相对好理解一点,主要解决的就是物流公司这一块,其实物流公司更像是一个集团,网点就像子公司一样,这里的子公司之间呢又存在业务及财务上的往来,所以说里面业务、功能权限、数据权限错综复杂。要解决这些问题并非容易。
为了加快熟悉产品业务,以及理解客户的需求。一上手并没有直接就开始开发新版本,而是基于老软件进行定制开发。现在1年多过去了,我还记得定制的有几个模块还是相当复杂的,加上老软件缺乏文档,代码又不规范且没有注释,最糟糕的是全是存储过程,开发起来难度很大,总结起来有一下几点。
- 看不懂别人的代码,不明白别人的意图
- 不熟悉业务,需求理解不透彻,导致不断返工
- 项目开发中节点不明确,被客户牵着鼻子走
- 软件遗留bug多,一边开发新功能还要一边解决历史bug
- 添加新功能,新模块影响到老功能,导致数据不准确,系统报错等一系列问题
这个定制项目,断断续续持续开发了3个多月才告终,做这样的项目真的很磨人的性格。但是这个项目做下来对系统的业务更熟悉了及客户的需求更清楚了,在大哥帮组下对老系统的表结构以及系统设计思想有了进一步的认知,做项目的过程中慢慢把公司中各个同事的职责也弄清楚了。就技术层面老软件很明显的用到的技术相对简单,客户端直接数据库,几乎全部是存储过程,UI方面用的是devexpress+winform(后面简称Dev),数据层用到的微软提供的企业库EnterpriseLibrary,嗯....感觉用这个企业库有点杀猪用牛刀的意思,企业库太重了,功能非常多,但是我们只用了调用存储过程的方法。期间对企业库有过一段时间的了解,由于涵盖内容较多加上技术比较老,用的人已经不多了,所以没有刨根问底的学习这玩意,紧紧处在用的层面,有时候想想还不如一个SqlHelper呢。
经过前面的铺垫终于开始新软件的开发之路,起初2个开发,没有项目经理、美工、测试。全部就是2个开发,一个就是我。还有一个是小呆萌(我学弟,刚刚毕业没有工作经验的那种)加1个经理,经理就是我们公司的boos(后续称大哥,还是挺佩服的,集技术、运维、售前、售后、管理于一身,也是为公司操碎了心 哈哈哈)。前期大哥貌似对项目不怎么上心,虽然参与到项目的开发里面来了,还是很多地方并不是很关心,只有核心技术点上与大哥有沟通。技术上选型是这样的,还是沿用老的winform+dev这一套,但是本质有了变化新产品仅仅把winform+dev用作前端UI开发,后端服务采用的是webapi+ef,数据库采用的是sqlserver(沿用老软件的设计思想 单数据库模式即所有的用户数据用同一个数据库、同一个数据表存放)。总的技术方向有了、总的需求有了下面就需要对系统进行细化,需要搭建框架。下面就从客户端、服务器2个方向入手,细谈第一版为何这样设计以及这样设计有什么弊端,后续如何把自己埋下的炸弹给挖掉。
客户端
基本是沿用的老方法,为兼容在XP系统上运行,选用的是基于.Net 4.0开发。由于.Net的版本限制,所以很多高级特性不能使用。简单封装了一些基本使用类,如序列化类(基于json.net)、api请求类(基于HttpWebRequest类封装)、日记记录(基于log4net)、缓存帮助类(基于mencached)、根据项目需要基本类型如Datetime、Int、String等类拓展了常用方法。客户端中由于考虑到一下情况,初版我做了如下规范:
- 所有需要调用接口的地方采取配置文件的形式,当时之所以会这样设计是应为考虑到,万一服务器控制器名或方法名发生变动可以在变动代码的前提下通过修改配置文件的形式实现快开发,这个想法出发点是好的,而且非常有利于代码的维护,但是没有考虑到代码不是自己一个人写,新开发的加入无疑增加培训成本。而且我自己开发过程发现需要在代码cs文件和配置文件中2个不同的地方来回切换很不方便浪费很多时间。
挖的第一个坑【采用大量配置文件导致开发效率低、培训成本大】
服务器
这边和老软件相比有了本质的变化,老软件的设计很无脑,实实在在的CS架构,客户端数据库直连的形式再加上存储过程。这里谈到存储过程,我谈谈自己开发定制项目自己的感受,存储过程最大的优势无疑是性能强悍,为什么这么说呢。主要是存储过程直接保存在数据库里面的,仅仅需要传输参数就可以而且sql本身会对其进行查询优化,就我的开发感受而言,我还没遇到哪个技术的处理速度能够高于存储过程的性能的。但是存储过程这玩意开发体验很差,首先需要熟悉sql语法,要熟悉sql基本函数,知道游标等等。所以说这是需要一个技术相对专业的人维护才行。但是实际情况是,面对小公司想找一个技术全面的人很难得,一般的开发在处理sql层面仅仅处在select、delete、update、add、where的层面,而将更多逻辑放在业务代码中处理。而且别人写的存储过程晦涩难懂,动不动上千行的存储过程实在让人看着绝望,嗯...绝望这个词用得好~~~哈哈哈。这么分析下来还是能得出结论了,存储过程这个技术是好的,但是弊大于利,以至于本次开发新系统直接摒弃该技术。但是不用存储过程带来的问题就是性能的大幅降低。为解决以上问题,结合自己积累的技术选择了基于别人一套的快熟开发框架来进行开发。核心技术点有持久层采用的EF 数据库优先,缓存采用的是Memcache、应用服务层采用webapi。就这样服务器基本就完成了,亮点技术及坑总结如下:
- 可能自己项目经验比较少,也有可能没做过业务有这么复杂的项目,让我从开发系统无非是CRUD(增删改查)的逻辑中脱离出来。为了快速开发(原因是人手不够、自己开发封装的水平又不高),在整个应用层面我采用了大量的T4模版结合EF框架实现的超乎想象的快速开发,只要数据库设计好,接下来只需要简单的配置,整个服务代码完全ok。但是这里需要注意的是需要一些约定,如主键的后缀一般采用表名+Id的命名规范等等。这种模式仅仅适用非常简单的业务代码开发,拿这个开发产品无疑是搬石头砸自己脚,随着项目的进行,业务不断复杂,我不断调整T4模版(写T4模版比写存储过程还恶心,这辈子再也不想写了)不断的加入各种各样的参数配置,使得生成的代码更能符合业务的需要。随着业务越来越深入,后来几个版本的迭代中我不断的把此技术剔除。
挖的第二个坑【采用大量T4模版,代码写代码的模式,然而并不知道,需求无时无刻不在变化】
- 由于干掉了存储过程,这里引入了分布式缓存的概念,在老系统中是没有这项技术的。为何要引入该技术呢,前面有提到软件速度慢。在没有缓存的系统中,数据是怎么呈现到用户眼前的呢?数据是读取硬盘中的文件加载到内存中最终呈现到用户的眼前,那么引入缓存的概念呢,直接读取内存,读到数据根据缓存键直接返数据否则取硬盘数据。由于读取内存中数据的速度远大于硬盘,所以说缓存的引入无疑是极大的提升系统的性能。原理虽然是这样,如何对数据进行缓存。哪些数据需要缓存?哪些数据不需要缓存?如何管理缓存键?等等一系列问题扑面而来。在系统开发过程中需要进行缓存的总结几点:1.不经常改动的数据;2.大量使用的数据。如果在系统中发现这样的数据,那么应该将他们进行缓存处理。比如:用户信息、公司信息、菜单信息、功能信息等等都是需要反复使用而不怎么修改的数据,我们应该进行缓存处理。试想:每次请求过来要进行身份校验、权限校验、数据校验等等都查询数据库的话,数据库压力可想而知。
- 由于框架中对数据上下文(ef连接对象)在数据层进行了缓存处理,在后续的开发过程中总是会出现许多莫名奇妙的错误,比如:EF常见的报错,百度一搜一大把的那种、明明修改成功了,再次查询后却是修改之前的状态等等一系列问题。出现这样的问题,我花费了大量时间去找资料、请教前辈来解决。在搞明白基本原理的前提下,后续几个迭代开发版本中,慢慢调整框架使他更适用。
挖的第三个坑【最求高大上技术(不是特别熟悉的技术)有时候并不能有效解决问题,往往还有可能存在风险】
- 数据层核心就是EF这里我对其进行了简单的二次封装比如(增删改查),关键点就是微软推荐我们先把数据查询出来,再进行数据的更新。但是实际使用的过程中发现这样效率并不是很高,而且生成的Sql不够简洁,比如一个表多大上百个字段时,我只想修改一个字段,ef生成的sql脚本确实更新所有字段。基于以上的分析,对ef拓展了很多实用的方法,以至于到现在,这些方法都是必不可少的核心。
- 写了个辅助程序,让实体类能够自动加载数据库中的字段注释。虽然是个小东西,但是能省下非常多的时间,留下更多的时间去做更有意义的事。
数据库
数据库是重中之重,软件设计的好不好就看数据库了,有时候会多加一个冗余字段你会能够让你少写很多代码,能够大幅提升系统性能。在实际经验下发现,综合考量下来遵循范式设计的系统要比不遵循范式设计的系统就性能和用人方面而言,相差很大。其实很多时候,记住一句经典的话“把数据库设计的和excel一样!”虽然这样并不是很科学,也许这样设计在学校里或其他公司中会成为笑柄,但是实际开发过程中的经验所得,确实是如此。简单的设计会免去你很多烦恼。相比较老软件而言新架构的大变动如下:
- 数据库主键的技术选型,sql标准主键3种方案:1.guid类型(优点:全球唯一,速度快,缺点:占资源,冗长,不利于索引维护);2.自增(优点:自动编码省心,缺点:主键自增的特性 数据迁移可能会有麻烦);3.用户自定义(注意不要重复,自己定义规则)。我们选用的是用户自定义,采用的bigint也就是长整型做主键,主键统一为18位。用的是SnowFlake,我自己给他起了个别名叫“分布式id生产器”,其实这个方案是大哥极力推荐的,但是实际使用中还是面临许多问题,首先看他名字就知道不简单。既然主键在客户端生成那么他的原理还是先有客户端请求服务才能拿到一个可用的主键,而SnowFlake通过机器码和数据中心2个产生支持多个服务器部署能够满足大并发。SnowFlake原理还是根据时间和那2个产生来动态生成主键的。仔细考虑后发现,多多少少会损耗性能的,至少需要网络传输呀。还有一个弊端就是分布式id生产器宕机会直接导致系统奔溃等等一系列的问题会暴露出来。但是幸运的是目前还没有发现这样的问题。原理图参见下图
- 默认数据表都应该含有的字段:租户标识、创建人、创建时间、修改人、修改时间、逻辑删除标识、备注。最初除中间表外我们规定每个表都应该还有这几个字段。基本每个字段都是比不可少的主要用于数据追溯,如客户发现数据不对了找到软件公司,软件公司应能够做到数据被修改原因的追溯,从而规避问题,将问题放到指定人身上去。能够知道最后是谁动数据的。逻辑删除用于用户误删除数据恢复。然后在后续的开发过程中发现会有同一人修改相同数据,这就没法保证数据的一致性,由于考虑不周加上自己的懒惰想当然的用修改时间来控制,前期没问题,很顺利,但是忽略了一个细节就是修改时间只是用户对数据进行修改时才会改变,那用户不修改数据,数据由于其他模块导致变动版本怎么控制?我们发现这个问题时,产品已经上线了。最终不得不把每个表额外加上一个标准字段版本号,这样才从根本上解决了问题。
挖的第四个坑【不要由于偷懒而忽略一些潜在风险,时间会将问题慢慢放大,如果不及时修改,可能导致产品研发失败】
基于以上思路带着小萌新开始了新软件开发之路,随着开发不断深入,把系统中的几条关键逻辑线路全部走了一遍。第一版ui基本就是拖拖控件出来的,没有太多华丽的设计,也没有考虑到用户体验。期间遇到大大小小很多问题,包括技术上、业务上都有。随着开发的深入,测试时会发现还会存在的性能的瓶颈,所有模块还是很慢。期间我看过ABP框架的设计结合老系统发现,如果需要彻底解决问题,只能分库。一个租户一个库,这样方便系统的拓展及分布式部署。租户与租户之间数据隔离,也保障了数据的安全,某个节点数据服务崩溃不会导致全线崩溃等等优势。凡事都是有利有弊,弊端就是数据库结构发生变化,子库需要同步等问题。但是综合考量下来,分库的优势很大。基于这样的分析,有想法得有行动,很快我就对系统进行重构。数据库改动较大,分成主库(也可以理解为路由库)和子库;应用服务器首次改动比较保守,没有伤筋动骨。但是随着开发的进行问题会暴露出来,问题是何时用主库上下文?何时用子库上下文?这么多数据库对象如何管理?等等问题全部暴露出来.....真的是牵一发而动全身。起初是通过配置文件进行控制,开发前几天发现还可以,但是随着开发的进行会发现真的是各种场景都会出现,同一接口中会出现主库上下文、子库上下文....等等一系列让人头皮发麻的问题,实在没有办法,对应用服务器做了很大的改动,干掉了2个封装的dll,直接3层,服务层->业务层->数据层。简化数据访问,重新封装了数据库访问接口,采取短连接的形式,一句话概括也就是“及时用,及时放”的策略,这样的设计架构。清晰明了,免去繁琐的配置。这次大改动足足化了一周的时间才完成,以至于这样的架构模式沿用至今都没有大改动,能够适应各种业务场景。哈哈哈 一周的努力没白费。经过这样的折腾,加上客户及Boss的压力明确提出产品要尽快上线。加上之前客户端不是我着手开发(前期写了点,后续全部交给别人了),服务端稳定一段时候后,接着着手客户端的开发。最终系统架构参见下图
直接上手就是对控件进行一顿2次封装,免去每增加一个控件时都需要设置一堆属性的烦恼。基本控件都是很顺利的完成。但是有的控件很复杂涉及很多东西,如 gridview控件要做到 样式的统一、默认右键菜单、悬浮按钮、焦点行 热点行的数据获取等等。起初由于开发人员较少,我这边没有经过大量测试直接将代码提交到开发环境供开发们使用。起先封装的少,代码也能够理解,没有暴露问题。渐渐的随着不断有新开发加入进来,会发现封装写的太死,引入新功能导致旧功能不能用的事故非常多,而开发总是埋怨我这边做的不够好。后续只能先自己做足测试,给开发做好培训工作之后才稳当的使用自定义控件。
挖的第五个坑【产品迭代开发到一定程度,涉及到核心的封装代码改动,一定小心再小心,否则等待的将是拆东墙补西墙,到处改动】
挖的第六个坑【封装的东西一定要活,写的不够灵活,后续迟早要还的】
在第二版本的开发过程中还会发现,在应用层接受数据时和应用层响应数据时,往往不是实体类,而是视图模型。这里就需要建立大量的视图模型,服务器响应好处理,直接返回匿名类。客户端接受服务器响应的数据直接采用反序列化就可拿到数据。关键在新增数据的时候 往往会出现 视图模型的数据需要赋值给实体模型,这些代码几乎无脑,但是他是存在的,无法规避。如果手动编码会占据大量的时间来维护。这里用到了对象转化利器AutoMapper,该组件通过简单的配置即可实现视图模型与实体模型之间的转化,剖析原理可能采取反射或者emit的技术实现的映射,为测试性能,我自己写反射与用AutoMapper进行性能测试发现AutoMapper性能更快,但是AutoMapper肯定没有手动赋值速度快,但是和手动写一堆代码而言,这点性能还是能够接受的。
经过这一圈的折腾从服务器写到客户端,再加上开发的努力,产品似乎有了点雏形。其实这才是开始,随着项目越做越大,主要还要面对2个问题。1.新模块的介入;2.旧需求的改动。其实这是2个很让人头痛的问题。你必须全盘考虑,系统各个模块之间数据的流转、同步;特别是库存数据和财务数据必须做到分毫不差。最让人头疼的就是旧需求的改动,其实这也是程序员(开发)非常抵触的,好不容易开发出来的模块,由于没有考虑好又被推翻重做内心坑定是崩溃的。做项目还好,需求入口的口径只有一个,关键是做产品,需求来自五湖四海,每一家的需求不一致,除了要考虑软件的健壮性,数据的准确性之外,还得考虑这个功能其他家是不是也需要,是不是也合理等等。盲目的迭代升级,往往会拖垮软件的性能,导致软件显得臃肿,一个好的产品应该易用,功能全面而又简单。
在我们设计软件的时候,我也服务过客户,很多时候客户的思维和专业软件设计人员的思维往往相差甚大。用户真的是什么操作都有,真的是喜欢这里点点那里点点,如果软件不够健壮,面临的将是到处异常。而且用户可能会因为少一个字段,多操作一个按钮而吐槽说软件不好用。而且使用软件各个层次的人都有,他们受文化教育程度也不一样,年龄也不一样。有的连基本的电脑使用都不顺畅(无法区分鼠标左右键的大有人在),年纪大的人可能会由于软件字体小而吐槽说软件不好用。所以呢,设计软件你得把用户当傻瓜对待,操作一目了然、减少用键盘、鼠标的操作的次数,这样设计出来的软件才能覆盖到更多的用户群体。
随着客户量的增长、开发团队的扩张,由于项目急于上线,很多模块需要开发,系统架构要优化,数据库表要管理再加上分库了,表结构同步问题,升级问题,软件测试等等一些问题。我犯的最大的错就是所有事都是自己干,核心代码自己写、普通业务代码自己写、小工具自己写(如:辅助升级工具、数据库同步工具、用户管理工具等等)。渐渐发现很多事情都做不好,自己很是力不从心。唯一的解决途径就是,一个要相信自己的团队有能力把事情做好,而且要相信他们能比我自己做的更好。只有不断的放权,相信一起开发的伙伴这才是体现团队价值的时候。
挖的第七个坑【切莫所有事情都自己躬亲做,把事情交给合适的人做,效果会更好】
在持续不断的和同事沟通,安排任务。协同开发过程慢慢发现,其实管人比管项目更难,换句话说也就是“情商远比智商”重要,不不不,这边不能用肯定句,应该分条件来区分了,要看自己处在什么层次,如果自己是刚踏入社会的小萌新,还是要专业知识强硬一点往往会好一点。等自己到管理层,情商远远比智商更重要。可以这么理解,对事用智商、对人用情商。再总结一句话:“情商高、智商高 如鱼得水;情商高、智商低 幸福生活;情商低、智商高 怀才不遇;情商低、智商低 路边乞丐”。我想大部分人都是包括我也是,刚踏入社会,很是任性,天不怕,地不怕。其实这样很难在社会立足。我本身就属于情商低,智商也不高的状态,摸索如何有效的管理公司以及让公司的产品能够快速开发上线也很艰难,以至于到目前为止我还是不能够有效的管理。
挖的第八个坑【切莫认为智商比情商更重要】
到目前为止公司的开发几乎全部加入到新产品的开发队伍中了,到目前为止业务上的代码几乎全部交给之前提到的小呆萌接手,虽然前期技术很一般,给人感觉能力也不够,慢慢培养并相信他有能力把事情做好。事实确实如此,基本能够把问题处理好。这里提到的基本,也只是基本。效率非常差劲,公司急需一条体系,就像生态圈一样平衡稳固发展的那种,但是我又没这方面的经验,不得不我请教了很多创业的老板、同学以及其他公司的老板等等。发现每家的管理体系都不相同,首先公司有大有小,大公司部门职责划分明确,部门多,员工绩效项目管理等体系完整但是这种模式并不适用,我们公司更像发展中的小公司一样。而且我们公司一致被我吐槽的一大诟病“非常不愿意写文档”,期间为管理好项目,协同办公尝试过不同的bug系统,在处理问题时效率是有所提升,但是还是不能按时完成任务,我们处理问题的一般流程是:测试测出一个问题->口头告知开发->开发核实确认->修复问题->升级系统,就是这样的模式进行,最大问题出在,1.开发不能够明确的理解错误原因;2.开发完成后不测试直接说没问题升级吧。导致一天升级次数达数10次,很多时间都浪费在系统发布上。由此还开会对开发反复强调这样的问题,问题还是有所改善的。有时候也会想到扣员工的绩效,出现问题追究负责人算绩效,我想大部分公司都会有这样的措施或方案,但是自己想想不到非常不得已还是不要这样好了,毕竟这些兄弟一起做这个系统,都是经历过从0走到1的,往往第一步是最难的,我也希望都能做好自己份内的事,我们公司“永远“不要出现绩效考核。
一直保持这样的节奏,很快项目上到生产环境。产品设计之初是准备有3个独立环境:开发环境(开发人员使用)、测试环境(测试人员使用)、生产环境(客户的真实使用环境)。由于频繁升级,加上自己的懒惰,没有好好利用测试环境。而是让测试人员直接在开发环境中测试,我就这样亲手给产品埋下了一颗炸弹,起初升级都很顺利。开发环境中测试这样的隐患非常大,直到有次,我们为加强系统安全性,对用户的密码进行密文保护后,开发环境中很顺利没问题,但是提交到生产环境时导致客户没办法升级,后果可想而知,所有用户无法完成自动升级,以至于化了2天公司客户、开发全部介入进来手动给用户升级...这段时间我是天天被老板骂的狗血淋头。当时就想想要是测试环境中测试就能发现这样的问题了。在这次事故中主要责任还是在我,没有保证好产品的稳定发展。除主要责任外,次要责任主要有开发代码不过关。考虑不全面等等。这次事故中主要总结一下几点。
- 测试永远不要相信开发的代码,在小的功能都要测试
- 不要被上级的压力,客户的需求乱了阵脚
- 不确保升级生产环境没问题,再催也不升级
直到测试环境正式用上后,不同部门的人在不同的环境中工作,到目前为止再也没出现过类始于上次那样的大错误。之前提到被老板骂的狗血淋头,这里老板骂的再凶这个锅也要接好,千万不要乱丢。为什么这么说呢,不能丢,大家伙都在为产品努力本身压力很大,在加上任务是我安排的,自然邀功主要人也是我,要是把锅甩出去,问题就大了,闹不好可能会出现手下伙伴不满,导致产品核心开发流失等等更大的问题出现,所以记住“背锅比邀功更重要”。只有这样才能让团队更好的发展。
后续随着产品的迭代、用户量的激增还会面临更大的挑战........哈哈哈
......
持续更新
......
后记
期间碰到的许多问题,如果全部拿出来分析怕是3天3夜也写不完~~,只是挑选一些比较大的问题阐述。小弟不才如有大佬能够指导如何更好的优化系统以及一些技术的推荐或建议可以给我留言~~~最后真心感谢大哥、公司各个业务员、开发同事的一起努力。
以上是关于产品开发经验总结-让你少奋斗一年的经验之谈的主要内容,如果未能解决你的问题,请参考以下文章