




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、如何提高产品开发品质如何提高产品开发品质讲解内容讲解内容目前开发过程存在的问题1产品开发品质保障流程2代码重构3测试驱动4日构建5一、目前开发过程存在的问题一、目前开发过程存在的问题l没有完善的品质保障流程没有完善的品质保障流程l质量低下的代码质量低下的代码l不重视测试不重视测试产品开发流程现状产品开发流程现状开发人员编写代码开发人员编写代码调试调试, 肉眼观察肉眼观察有问题有问题没问题(自认为)没问题(自认为)可交付的代码可交付的代码编译编译修正编译错误修正编译错误登记到登记到JIRAJIRA上上客户提出需求或客户提出需求或现场发现现场发现bugbug产品品质管理严重缺失产品品质管理严重缺失
2、 该流程的最终目标和工作重心都是完成产品功能开发,品质管理严重缺失。产品的开发质量完全靠开发人员的个人责任心以及工作经验来保障,缺乏一个稳定可靠的质量保障流程。这种流程开发出来的产品往往是质量不可靠,需要经常返工的劣质产品。质量低下的代码是导致产品品质质量低下的代码是导致产品品质不好的根本原因不好的根本原因质量低下的代码体现在以下几个方面:l重复的代码l过长的函数l过大类l过长的参数列表l过度复杂的逻辑判断l数据泥团没有专职测试人员来进行功能性没有专职测试人员来进行功能性测试工作测试工作在目前的开发流程中,开发出来的代码只经过开发人员自己简单的测试,没有专职的测试人员来进行详细的功能性测试。这
3、样导致的结果往往是提交到现场的代码会带有不少BUG,一定要经过多次返工才能达到较高的品质。而且这样做的另一个后果是把客户当作测试人员,会给客户留下产品品质不稳定这样一种非常不好的客户体验。开发人员在开发过程中不重视单开发人员在开发过程中不重视单元测试元测试单元测试是提高产品品质非常重要的一个方法,而我们的开发人员往往会忽视这一点。如果没有单元测试,仅仅依靠测试人员的功能性测试,那么这样的测试工作量会非常大,每次修改一个功能,可能会影响到的其他功能都要一一测试,不仅测试时间会非常长,而且效果也不好,很多细节不一定每次都能测到,这些都是产生BUG的隐患。同时由于我们业务需求都非常复杂多变的,没有一
4、个完善自动化测试流程,而仅仅依靠人工测试,对产品品质的影响是不言而喻的。我们的目标!我们的目标!l编写出逻辑清晰、结构简洁、扩展性良好、可测试性高的优秀代码。l强化单元测试工作,提高单元测试覆盖率,搭建自动单元测试集,通过日构建来持续集成,对产品质量进行全面控制。l通过严格的产品质量管理流程,强化质量管理工作,将所有BUG消灭在公司内部。二、产品开发品质保障流程二、产品开发品质保障流程开发质量管理开发质量管理本开发流程的目标是开发出质量优良的产品,流程的重心在于质量管控,通过质量保障人员对产品质量进行全面把关。l对于没有编写单元测试的代码直接打回!l质量保障人员负责进行功能性测试,并对提交出去
5、的代码负责。l质量保障人员另一个职责是思考如何持续改进产品质量。需求管控需求管控l对每个需求进行分级评审,最大程度的降低需求变更的频度。l所有需求开发前都经过内部评审,对于一些复杂需求把握更加准确,不至于在开发时候产生较大偏差。l所有需求的开发工作都有经过客户签字的开发工作量评估,为商务工作开展创造有利条件。l所有需求都有详细开发计划,片区人员可以安排相应的测试计划。全过程管控全过程管控l所有缺陷和经过评审的需求都必须在JIRA上登记,否则不予开发。l开发计划通过JIRA进行精确体现。l片区人员可以通过JIRA实时跟踪产品开发进度。l方便后续各类工作量的统计。三、利用代码重构来提高代码质量三、
6、利用代码重构来提高代码质量 重构(Refactoring):是对软件内部结构的一种调整,目的是在不改变外部行为的前提下,提高其可理解性,降低其修改成本为什么重构(为什么重构(1)l改进软件的设计。l程序员对代码所做的为了满足短期利益代码改动,或再没有完全清楚增个架构下的改动,都很容易是代码失去它的清晰结构,偏离需求或设计。而这些改动的积累很容易使代码偏离它原先设计的初衷而变得不可立即和无法维护。l重构则帮助重新组织代码,重新清晰的体现结构和进一步改进设计。为什么重构(为什么重构(2)l提高代码质量,更易被理解l容易理解的代码可以很容易的维护和做进一步的开发。即使对写这些代码的程序员本身,容易理
7、解代码也可以帮助容易地做修改。程序代码也是文档。而代码首先是写给人看的,让后才是给计算机看的。l重构帮助尽早的发现错(Bugs)l重构是一个code review和反馈的过程。在另一个时段重新审视自己或别人代码,可以更容易的发现问题和加深对代码的理解。l重构是一个良好的软件开发习惯。为什么重构(为什么重构(3)l重构可以提高开发速度l重构对设计和代码的改进,都可以有效的提高开发速度。好的设计和代码质量实体提高开发速度的关键。在一个有缺陷的设计和混乱代码基础上的开发,即使表面上进度较快,但本质是试延后对设计缺陷的发现和对错误的修改,也就是延后了开发风险,最终要在开发的后期付出更多的时间和代价。l
8、项目的维护成本远高于开发成本.何时重构何时重构? ?l添加或者修改功能时一并重构l为了增加一个新的功能或者修改原有的功能,程序员需要首先读懂现有的代码。l修补错误时一并重构l为了修复一个Bug,程序员需要读懂现有的代码。lCode Review时一并重构何时不该重构何时不该重构? ?l代码太混乱,设计完全错误。与其Refactor,不如重写。l明天是DeadLinel永远不要做Last-Minute-Change。推迟重构,但不可以忽略,即使进入产品期的代码都正确的运行。重构方法介绍:提取函数(重构方法介绍:提取函数(1)String name = request.getParameter(N
9、ame);if( name != null & name.length() 0 ).String age = request.getParameter(Age);if( age != null & age.length() 0 ).String name = request.getParameter(Name);if( !isNullOrEmpty( name ) ).String age = request.getParameter(Age);if( !isNullOrEmpty( age ) ).private boolean isNullOrEmpty( final St
10、ring string )if( string != null & string.length() 0 )return true;elsereturn false;重构方法介绍:提取函数(重构方法介绍:提取函数(2) 提取函数是我最常用的重构手法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立的函数中。有数个原因造成我喜欢简短而有良好命名的函数。首先,如果每个函数的粒度都很小,那么函数之间彼此复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再者,如果函数都是细粒度,那么函数的覆写(override )也会更容易些: 的确,如果
11、你习惯了看大型函数,恐怕需要一段时问才能适应达种新风格,而且只有当你能给小型函数很好地命名时,它们才能真正起作用,所以你需要在函数名称下点功夫,一个函数多长才算合适?在我看来.长度不是问题,关键在于函数名称和函数本体之间的语义距离,如果提炼动作可以强化代码的清晰度,那就去做,就是函数名称比提炼出来的代码还长也无所谓。重构方法介绍:去除临时变量重构方法介绍:去除临时变量(1)(1)重构方法介绍:去除临时变量重构方法介绍:去除临时变量(2)(2)我喜欢尽量除去函数内的临时变量。临时变量往往形成问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易失去它们的踪迹,尤其在长长的函数之中更
12、是如此。而且,临时变量的存在,往往会阻碍提取函数等其他重构手法的进行。重构方法介绍:重新命名函数重构方法介绍:重新命名函数(1)(1)public String getItemName (int itemSort,String itemName)return +itemSort+、+itemName; public String formatItemName (int itemSort,String itemName)return +itemSort+、+itemName; 重构方法介绍:重新命名函数重构方法介绍:重新命名函数(2)(2)我极力提倡的一种编程风格就是:将复杂的处理过程分解成小函
13、数。但是,如果做得不好,这会使你费尽周折却弄不清楚这些小函数各白的用途、.要避免这种麻烦,关键就在于给函数起一个好名称;函数的名称应该准确表达它的用途;给函数命名有一个好办法。首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。人生不如意十之八九:你常常无法第一次就给函数起一个好名称,这时候你可能会想,就这样将就着吧,毕竟只是一个名称而已。当心!这是恶魔的召唤,是通向混乱之路,千万不要被它诱惑!如果你看到一个函数名称不能很好地表达它的用途,应该马上加以修改。记住,你的代码首先是为人写的,其次才是为计算器写的。而人需要良好名称的函数。想想过去曾经浪费的无数时间吧,如果给每个函
14、数都起一个良好的名称,也许你可以节约好多时间。取一个好名称并不容易,需要经验,要想成为一个真正的编程高手,取名称的水平是至关重要的。重构方法介绍:以多态取代条件重构方法介绍:以多态取代条件表达式(表达式(1 1)public class PlanUtil public void setPlanState(int planYear,int itemId,int dataType)switch dataType case 1:setPrePlanState(planYear,itemId); case 2:setColPlanState(planYear,itemId); case 3:setAf
15、tPlanState(planYear,itemId); default:throw new RuntimeException(不正确的计划类型:+dataType);/.更新建议计划状态public void setPrePlanState /.更新综合计划状态public void setColPlanState. /.更新开工计划状态public void setAftPlanState.public class PlanUtil public static PlanUtil create (int dataType)switch (dataType)case 1:return new
16、PrePlanUtil();case 2:return new ColPlanUtil();case 3:return new AftPlanUtil();default:throw new RuntimeException(不正确的计划类型:+dataType);public abstract void setPlanState (int planYear,int itemId,int dataType);public abstract void savePlan (int planYear,int itemId,int dataType);重构方法介绍:以多态取代条件重构方法介绍:以多态取
17、代条件表达式(表达式(2)/保存计划数据public void savePlan(int planYear,int itemId,int dataType)switch dataTypecase 1:savePrePlan(planYear,itemId);case 2:saveColPlan(planYear,itemId);case 3:saveAftPlan(planYear,itemId); default: throw new RuntimeException(不正确的计划类型:+dataType);/.保存建议计划数据public void savePrePlan /.保存综合计划
18、数据public void saveColPlan/.保存开工计划数据public void saveAftPlanpublic class PrePlanUtil extends PlanUtil public void setPlanState (int planYear,int itemId,int dataType). 更新建议计划状态方法public void savePlan (int planYear,int itemId,int dataType)保存建议计划数据方法public class ColPlanUtil extends PlanUtil public void se
19、tPlanState (int planYear,int itemId,int dataType). 更新综合计划状态方法public void savePlan (int planYear,int itemId,int dataType)保存综合计划数据方法重构方法介绍:以多态取代条件重构方法介绍:以多态取代条件表达式(表达式(3)/调用更新计划状态的方法planUtil.setPlanState(year,itemId,dataType);/调用保存计划数据的方法planUtil.savePlanyear,itemId,dataType);public class AftPlanUtil
20、extends PlanUtil public void setPlanState (int planYear,int itemId,int dataType). 更新开工计划状态方法public void savePlan (int planYear,int itemId,int dataType)保存开工计划数据方法/调用PlanUtil planUtil = PlanUtil .create(dataType);/调用更新计划状态的方法planUtil.setPlanState(year,itemId,dataType);/调用保存计划数据的方法planUtil.savePlan (ye
21、ar,itemId,dataType);重构方法介绍:以多态取代条件重构方法介绍:以多态取代条件表达式(表达式(4)l多态最根本的好处就是:如果你需要根据对象的不同型别而采取不同的行为,多态使你不必编写明显的条件式。正因为有了多态,所以你会发现:针对type code(型别码)而写的switch语句,以及针对type string(型别名称字符串)而写的if-then-else语句在面向对象程序中很少出现。l多态能够给你带来很多好处。如果同一组条件式在程序许多地点出现,那么使用多态的收益是最大的。使用条件式时,如果你想添加一种新型别,就必须杳找并更新所有条件式。但如果改用多态,只需建一个新的s
22、ubclass并在其中提供适当的函数就行了。class用户不需要了解这个subclass,这就大大降低了系统各部分之间的耦合程度,使系统升级.更加容易。重构方法介绍:以委托取代继承重构方法介绍:以委托取代继承(1)(1)/继承解决方案public classBusPlanManagerServiceImpl public void setPlanState (int planYear,int itemId,int dataType). 更新建议计划状态方法 public void savePlan (int planYear,int itemId,int dataType)保存建议计划数据方法
23、public class PrePlanService extends BusPlanManagerServiceImpl .public class ColPlanService extends BusPlanManagerServiceImpl .public class AftPlanService extends BusPlanManagerServiceImpl ./委托解决方案public classBusPlanManagerServiceImplpublic void setPlanState (int planYear,int itemId,int dataType)Plan
24、Util planUtil = PlanUtil .create(dataType);/调用更新计划状态的方法planUtil.setPlanState(year,itemId,dataType);public void savePlan (int planYear,int itemId,int dataType)PlanUtil planUtil = PlanUtil .create(dataType);/调用更新计划状态的方法planUtil. savePlan (year,itemId,dataType); 重构方法介绍:以委托取代继承重构方法介绍:以委托取代继承(2)(2)l继承是一件
25、很棒的事,但有时候它并不是你要的。常常你会遇到这样的情况:一开始你继承了一个class ,随后发现superclass中的许多操作井不真正适用于subclass。这种情况下你所拥有的接口并末真正反映出class的功能。或者,你可能发现你从superclass中继承了一大堆subclas并不需要的数据,抑或者你可能发现superclass中的某些protected函数对subclass并没有什么意义。l你可以选择容忍,并接受传统说法:subclass可以只使用superclas、功能的一部分。但这样做的结果是:代码传达的信息与你的意图南辕北辙,这是一种混淆,你应该将它去除。如果以委托取代继承,你
26、可以更清楚地表明:你只需要受托类的一部分功能。接口中的哪一部分应该被使用,哪一部分应该被忽略,完全由你主导控制。这样做的成本则是需耍额外写出委托函数,但这些函数都非常简单,极少可能出错。重构方法介绍:引入参数对象重构方法介绍:引入参数对象(1)public List getYearPlanList (String operatorID, String planYear,String iSeason, String planType,String dataType).public List getLastYearPlanList (String operatorID, String planYe
27、ar,String iSeason, String planType,String dataType)public void fillPlanData (String operatorID, String planYear,String iSeason, String planType,String dataType)PlanData planData=new PlanData();planData.setYeraPlanList(getYearPlanList (operatorID,planYear, iSeason,planType,dataType);planData.setLastY
28、eraPlanList(getLastYearPlanList (operatorID,planYear, iSeason,planType,dataType);public class PlanParamObj private String operatorID;private String planYear;private String iSeason;private String planType,;private String dataType;public PlanParamObj (String operatorID, String planYear,String iSeason,
29、 String planType,String dataType) this.operatorID=operatorID;this.planYear=planYear;this.iSeason=iSeason;this.planType=planType;this.dataType=dataType;重构方法介绍:引入参数对象重构方法介绍:引入参数对象(2)public List getYearPlanList (PlanParamObj paramObj).public List getLastYearPlanList (PlanParamObj paramObj).public void
30、fillPlanData (String operatorID, String planYear,String iSeason, String planType,String dataType)PlanParamObj paramObj = new PlanParamObj (operatorID,planYear, iSeason,planType,dataType);PlanData planData=new PlanData();planData.setYeraPlanList(getYearPlanList (paramObj);planData.setLastYeraPlanList
31、(getLastYearPlanList (paramObj );重构方法介绍:引入参数对象重构方法介绍:引入参数对象(3)l你常会看到特定的数组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属同个class,也可能隶属不同的classes。这样一组参数就是所谓的data Clump(数据泥团),我们可以运用一个对象包装所有这些数据,再以该对象取代它们。哪伯只是为了把这些数据组织在一起,这样做也是值得的。本项重构的价值在于缩短了参数列的长度,而你知道,过长的参数列总是难以理解的。此外,新对象所定义的访问函数还可以使代码更具一致性,这又进一步降低了代码的理解难度和修改难度。
32、l本项重构还可以带给你更多好处。当你把这些参数组织到起之后,往往很快可以发现一些可被移至新建class的行为。通常,原本使用那些参数的函数对那些参数会有一些共通措施,如果将这些共通行为移到新对象中,你可以减少很多重复代码重构方法介绍:函数迁移重构方法介绍:函数迁移(1)(1)public class RepUtilFunc private List initFundList().this.copyFundView(fView,planView); /汇总投资计划数据private void copyFundView (FundView fView,PlanView planView)fView
33、.setFund(fView.getFund()+planView.getFund();fView.setUpprefund(fView.getUpprefund()+planView.getUpprefund();fView.setDownprefund(fView.getDownprefund()+planView.getDownprefund();public class RepUtilFunc private List initFundList().fView.copyFundView(planView); /汇总投资计划数据重构方法介绍:重构方法介绍:函数迁移函数迁移(2)(2)pu
34、blic class FundView private double fund;private String upprefund; private String downprefund;getset方法public class FundView private double fund;private String upprefund; private String downprefund;getset方法public void copyFundView (PlanView planView)this.setFund(this.getFund()+planView.getFund();this.
35、setUpprefund(this.getUpprefund()+planView.getUpprefund();this.setDownprefund(this.getDownprefund()+planView.getDownprefund();重构方法介绍:重构方法介绍:函数迁移函数迁移(3)(3)函数迁移是重构理论的支柱。如果一个class有太多行为,或如果一个class与另一个class有太多合作而形成高度耦合,我就会迁移函数。通过这种手段,我可以使系统中的classes更简单,这些classes最终也将更干净利落地实现系统交付的任务。常常我会浏览class的所有函数,从中寻找这样的
36、函数,使用另一个对象的次数比使用自己所驻对象的次数还多,就会进行函数迁移。重构方法介绍:将过程化设计转重构方法介绍:将过程化设计转换为面向对象设计换为面向对象设计l有时间的话用一个实际例子进行展示。四、测试驱动开发方法简介四、测试驱动开发方法简介测试驱动开发(Test Driven Development,英文缩写TDD)是极限编程的一个重要组成部分,它的基本思想就是在开发功能代码之前,先编写代码的单元测试用例。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试用例的编写,然后编写相关的代码满足这些测试用例。循环进行添加其他功能,直到完成全部功能的开发。代码整洁可用(c
37、lean code that works) 是测试驱动开发所追求的目标。测试驱动开发优点测试驱动开发优点(1)(1)l需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。 l通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计
38、通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。 l开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。 测试驱动开发优点测试驱动开发优点(2)(2)l快乐工作的基础就是对自己有信心,对自己的工作成果有信心。当前很多开发人员却经常在担心:“代码是否正确?”“辛苦编写的代码还有没有严重bug?”“修改的新代码对其他部分有没有影响?”。这种担心甚至导致某些代码应该修改却不敢修改的地步。测试驱动开发提供的测试集就可以作为你信心的来源。
39、l当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。 l我的一段功能非常复杂的代码使用TDD开发完成,真实环境应用中只发现几个bug,而且很快被定位解决。您在应用后,也一定会为那种自信的开发过程,功能不断增加、完善的感觉,迅速发现、定位bug的能力所感染,喜欢这个技术的。 测试驱动开发基本过程测试驱动开发基本过程l明确当前要完成的功能。可以记录成一个 TODO 列表。l快速完成针对此功能的测试用例编写。l测试代码编译不通过。l编写对应的功能代码
40、。l测试通过。l对代码进行重构,并保证测试通过。l循环完成所有功能的开发 测试驱动开发案例测试驱动开发案例(1)(1) 需求:实现Fibonacci数列Fibonacci数列简介:Fibonacci数列从第0项开始依次为0,1,1,2,3,5,8的数列,它存在如下特点:第0,1个数为0,1。从第2个数开始,该数是前面两个数之和。 测试驱动开发案例测试驱动开发案例(2)(2) 先编写测试代码 : import junit.framework.TestCase; public class FibonacciTest extends TestCasepublic void testFibonacci
41、() FibUtil fb = new FibUtil(); 编写完测试代码之后在Eclipse中运行该测试类,发现Junit运行出错(显示了一条红色杠) 这是在预料之中,因为我们还没有编写FibUtil类。测试驱动开发案例测试驱动开发案例(3)(3) 为了使测试通过,那么下面开始编写FibUtil类public class FibUtil 然后再次运行测试类。这时会发现测试成功。 测试驱动开发案例测试驱动开发案例(4)(4) 下面增加测试用例,开始测试Fibonacci数列的实现函数fib:public void testFibonacci() FibUtil fb = new FibUtil(); assertEquals(0,fb.fib(0); 由于出现编译错误,所以需要在FibUtil类中增加fib方法如下:public int fib ( int i) return 0 ; 运行测试用例,通过! 测试驱动开发案例测试驱动开发案例(5)(5) 继续增加测试用例,判断1的情况:public void testFibonacci() FibUtil fb = new FibUtil(); assertEquals(0
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 二零二五版房产质押合同范例
- 二零二五幼儿肖像权协议书
- 个体户合伙协议书
- 电子商务中合同法的适用与保护二零二五年
- 二零二五企业搬家搬厂合同模板
- 2025南京市房屋买卖合同书-CEF
- 2025电子产品买卖合同范本
- 2025企业经营贷款担保合同
- 2025中外合作项目施工合同
- 2025年四川省劳动合同
- 退役军人无人机培训宣传
- 退役军人保密教育
- DB44∕T 370-2006 东风螺养殖技术规范繁殖与苗种培育技术
- 7.1我国法治建设的历程 课件高中政治统编版必修三政治与法治
- 2025年仲裁法考试试题及答案
- 2025年电梯修理作业证理论考试练习题(100题)含答案
- 交通运输部南海航海保障中心推迟公开招聘笔试高频重点模拟试卷提升(共500题附带答案详解)
- T-ZJWL 001-2024 大宗商品供应链金融动产质押监管仓储服务规范
- 交通运输行业股权分配方案
- MOOC 跨文化交际通识通论-扬州大学 中国大学慕课答案
- (正式版)SHT 3078-2024 立式圆筒形料仓工程设计规范
评论
0/150
提交评论