软件开发最佳实践(伪)

本文档是是记录项目开发过程中遇到的问题和解决方法的记录,为其他项目开发过程作参考。

起因

笔者团队是由大学生和研究生组成的,跟随老师和研究生学长开发了一个项目。此前团队内并没有开发项目的经验,甲方也没有提供相应支持的经验,二者都是两眼一抹黑,最后导致整个项目的开发体验非常地稀碎。这种稀碎不只是甲方的问题,也有团队内部的问题,在开发中对我们双方都造成了很大的困扰。

在经历了比较折磨的开发流程后,我们终于是把产品交付了,在所有的后置工作都完成之后,在老师的建议下,我们决定对项目从需求分析到最终交付的全过程中所遇到的各种问题和收获合成一个文档,供老师和学长所在的研究团队的其他项目进行参考。

内容

正如上文所说,本文档中包含了我们项目开发过程中遇到的所有问题,同时也包含了在开发过程中我们不断地修正项目得到的经验。这些内容比较粗浅甚至可能包含了大量的错误,但我们还是选择将这些经验以文字的形式共享出来,一方面既是对自己的警示,也是为其他人提供一个参考,另一方面也希望对此类问题比较熟悉的大牛斧正该文档的内容。

请注意:本文中并不会或只会有限的的包含对某些名词的一般性解释,主要是写我们粗浅的经验,故并不适合对项目开发或编码,设计等没有任何经验的同学查看或请结合其他资料进行理解。

我们将这个文档分为了几个部分,分别是:

  • 分析
  • 设计
  • 编码
  • 测试
  • 部署
  • 文档
  • 杂项

每个部分都有一个或几个子项,基本涵盖了软件开发的全过程,谨供各位参考。

实例简介

本文的实例为本团队开发的该项目,在以后的文章中均以本项目指代。

本项目是实现一个使用大数据相关技术对某人进行分析的软件,从形式上是某单位某部门的现有业务流程的enhancement。本项目使用了Springboot, Mybatis-plus, SpringData JPA,SpringData Neo4j,ActiveMQ ,Spark,Redis,OpenResty没有使用日志框架。

本项目分为五个部分:前端,后端,图数据库及相关后端,算法微服务以及网关

本文档的初版笔者是参与了软件开发的lony2003,kongqingxia,以及DymaticPro(TODO:问他Github昵称)编写,其他的贡献者请参考本文的Github页面。

同时本文档欢迎本科研团队的所有同学或老师参与完善,也欢迎其他同学、老师提出问题和修改意见。

本文开源在Github中并部署于Github Pages,感谢微软和Github给我们这个白嫖的机会 [doge]

本文遵循 CC BY-NC-SA 4.0 (署名-非商业性使用-相同方式共享 4.0 国际) 协议开源,使用时请遵守相关条款。

Powered by mdBook

需求分析

方法

需求分析是项目管理和软件开发中至关重要的阶段,它确保项目团队对项目目标和用户需求有清晰的理解。在进行需求分析时,需要注意以下几个关键方面:

明确定义项目范围: 确保明确项目的整体范围和目标。理解项目的边界和涵盖的功能,防止在后续阶段发生范围蔓延。 与利益相关者沟通: 与项目相关的各方进行充分的沟通,包括项目发起人、最终用户、业务分析员等。了解他们的期望和需求,以便更好地满足他们的期望。 识别和管理需求: 将需求划分为不同的类型,例如功能性需求、非功能性需求和约束性需求。确保需求是可管理的,能够在整个项目周期内进行追踪和变更。 建立优先级: 对需求进行优先级排序,以确保在有限资源下首先满足最重要的需求。这有助于确保项目在有限的时间内交付关键功能。 验证和确认需求: 通过与利益相关者的验证和确认,确保对需求的理解是准确的。这有助于避免由于误解或沟通不足而引起的问题。 考虑未来的可扩展性: 需求分析时要考虑到未来的变化和扩展。尽量设计灵活的系统,以便在将来轻松地适应新的需求。 避免过度工程: 避免在项目中包含不必要或过度复杂的功能。确保每个需求都有其明确的业务目的,并能够为项目的成功做出贡献。 文档化和追踪: 将所有需求详细记录,并确保在项目的整个生命周期中进行跟踪。这有助于团队了解项目的状态,追踪变更,并提供清晰的项目文档。 风险管理: 识别和评估与需求相关的风险。这有助于提前预测潜在的问题并采取适当的措施来降低风险。 团队协作: 确保需求分析是一个团队协作的过程。涉及到的团队成员应该共同参与,确保他们对需求有共同的理解。

我们总结了需求分析的流程如下:了解现状->了解痛点->了解形式->了解边界->提出解决方案。

其中了解项目的开发形式和了解边界是我们觉得对开发的影响最大的部分,如本项目其实一直对于项目形式不是很明确(其实这其中一大部分原因是因为我们也不知道我们能拿到什么数据和API,强烈建议需求分析过程中一定要明确的了解到底能拿到什么,这对设计流程是一大助力),开发过独立的系统和现有系统进行集成,也提出过对原有的系统进行改造,最后还是开发了一个独立的系统。

了解边界主要是确定开发什么不开发什么,笔者个人认为这是对本人影响最大的东西,本人就经常对项目做出超出边界的构想,这在实践中其实是有问题的,如果新的项目需要做需求分析强烈建议确定好边界并以文档的形式固定下来。

成本分析

成本分析对于我们开发人员其实比较遥远,不过笔者曾经参与过一个项目的部分成本核算过程,故记录在此。

我们对于成本的分析其实非常简单。我们将整个系统开发的流程拆分成若干个模块,包含前期分析设计,各模块编码过程,测试和部署等,每个模块根据任务量的多少分配时间(以周计算)和人员,故每个模块的人员成本为

模块人员成本 = 时间(周)* 人数 * 每周工资

以此为依据,总成本也就可以通过如下的形式表达:

成本 = (模块人员成本相加 * 1.5)+ 其他成本(资源,设备等购置)

这里在人员成本中乘以了1.5这个常数,这是因为作为开发人员,我们在未开发之前总倾向于过于相信自己的实力,导致对于时间的估计容易失真,特别是容易少估,所以乘了一个常数稍作修正,如果觉得1.5太多也可以适当的减少,不过最好不要少于1.3(是真的做不完QAQ)

题外话:都说对于大学生来说实习盖章和学分是支付报酬的良好形式,可能这就是老师们喜欢找学生们干活的原因吧[doge](不过我们老师并没有这么。。。呃。。。恐怖,我们开发每周都有钱拿,我觉得还是非常不错滴)

UI设计

TODO

API设计

这个部分没什么好说的,参照行业内的规范即可,本项目中使用的是类似于Restful的形式,舍弃了一定的表意元素,只是用GET和POST请求方式,以及POST请求使用200而不是201作为状态码,其他部分和Restful基本一致。在API文档的选择上,我们选择了OpenAPI 3.1的格式,使用swagger进行展示,使用Apicurio Studio平台进行设计。

相关文档:

一文搞懂什么是RESTful API - 知乎

OpenAPI Specification v3.1.0

Apicurio Studio

微服务设计

做微服务设计一方面是为分布式做准备,另一方面也是对于异构的项目来说必须经理的设计。本项目中使用了springboot,scala(spark),python作为项目语言,且在实践过程中发现springdata jpa和springdata neo4j存在冲突,需要分开,故必须对项目的微服务进行设计。

本项目中微服务间交互使用http协议,使用openfeign封装好的方式实现请求,该方案无需对编码人员增加太多的负担,编码人员只需要完成http接口的编写即可进行rpc,这是非常好的rpc方案。当然,涉及到多语言(Java,Go,Rust等混合)同时使用的时后,也可以去寻找其他普适性的框架,如grpc等,但就本项目而言,并不需要grpc来提升性能,相比Spark实时计算造成的耗时,json解析的耗时实在是不算什么了。

安全性设计

项目的安全性设计是项目设计中很重要的部分,即使你的项目并不安全你也要设计的看起来很安全。这其中有一定的技巧,比如引入验证码,https,操作确认等前端组件的设计,又比如良好的鉴权逻辑。最简单的鉴权逻辑应该是在网关层设置一层拦截过滤器,对不带token的请求进行拦截,这种操作是网关几乎都有的功能,查阅你选择的网关即可。

关于账号登录的设计,我们的建议是参考OAuthOpenID Connect等成熟的协议并加以适当的增删和修改,必要的时候可以使用keycloak等服务来进行账号登录逻辑的开发。

对于token,如果是单后端,没有微服务,没有分布式,那其实session和jwt都是不错的选择,但像本项目一样存在多个后端的话,那最好还是使用jwt作为鉴权的token,或者使用redis存储session信息,不要使用原生的session。

选型

项目的选型其实非常简单,只要遵循如下的原则:

  • 除了项目核心功能的实现外,所有的选型全部选择成熟的开源项目
  • 选择文档完善,用的人较多的开源项目
  • 如果你不确定该项目是否可用,那就先选择它,但注意要在其上再封装一层,方便随时替换
  • 重要的部分选择重量级的项目,不太重要的部分选择轻量的项目
  • 选择简单易用好理解的开源项目

在本组新的项目选型的方面,推荐大家使用如下的框架(这也是我们实践过比较方便的脚手架):

  • Java后端开发:集成式的框架建议使用ruoyi(新项目一般使用ruoyi的Vue版本),如果需要一些其他比较奇怪的数据库的话(比如NoSQL和图数据库)建议使用SpringData,RPC框架如果主要是在Springboot框架下开发的话建议使用dubbo,如果需要在其他语言或框架下做开发的话可以使用openfeign去连接(HTTP协议,开发较为简单)或者grpc(现在Springboot的grpc支持非常不错了),消息队列如果对性能要求不是很高的情况下,使用ActiveMQ内置的队列可能会更方便部署,如果需要多语言支持的话,Kafka,RabbitMQ可能更好一些。 注:其实现阶段Dubbo的生态也非常不错了,异构开发其实也可以考虑dubbo,不过没在其他语言中用过dubbo,所以具体怎么样笔者也不知道
  • python后端开发:python后端目前没有用到过比较集成的框架,毕竟目前python在组内是使用都是做微服务的,这部分使用Sanic这种框架可能比Django更方便,性能也不差很多,消息队列的话如果是python微服务内部使用的话推荐使用RQ框架,这个更方便一些,和组内长时任务的需求也比较相符合。
  • 前端开发:前端肯定是Vue体系了,如果使用ruoyi就参考ruoyi的开发流程操作就好,图表这边,图结构推荐使用AntV G6,其他图标推荐使用EChart
  • 网关层:网关这边没什么好说的,OpenResty莽就完事了,如果需要成品的框架的话可能Kong是一个不错的选择。市面上也有一些Java的网关,不过没用过,不好评价。据说Cloudflare那边计划把他们开源的Rust库扩写成一个网关(主要是反向代理和缓存方面的,对权限方面可能不那么完善)如果在开启新项目的时候碰巧他们已经production ready了可以调研调研尝试一下。

项目结构

项目结构是本项目二阶段接手后最大的问题。结构不清晰,命名模糊,过度分包等问题在本项目第一阶段代码中得到了充分的体现。项目结构本应该是设计阶段就确定好的,但编码习惯有可能将设计完善的代码结构变得再次混乱。在二阶段的开发过程中,我们遵循了如下的开发规范:

  • Controller层不包含任何业务逻辑,只作为Service层代码的wrapper使用,具体业务逻辑均在Service层中实现。

  • Service层分为两层,ControllerService和Service,ControllerService调用Service,通常一个ControllerService可以调用多个Service,Controller调用ControllerService,通常一个Controller只能调用一个ControllerService。

  • Service层所有基础类均为接口,另有单独的Impl类做具体实现(Impl类注册时注册为接口名称)

    我们强烈建议在新项目开发时也遵循上述的规则,既方便替换,也方便生成假数据。

封装

Java封装的主要作用

Java封装的主要作用是保护数据的安全性和完整性,提高代码的可维护性和可扩展性,简化代码的调用过程。

封装是Java中的一种重要面向对象编程特性,它通过将数据和操作数据的方法封装在一个类中,对外部隐藏实现的细节,只暴露必要的接口。这样做的好处包括:

  1. 防止数据被非法访问或篡改:通过将对象的状态信息隐藏在对象内部,并限制对其访问的方式和范围,可以有效避免数据被非法访问或篡改,从而保证了程序的安全性和稳定性。

  2. 提高代码的可维护性和可扩展性:封装使得对象的实现细节对外部无需暴露,这样就可以随意修改内部实现细节而不会影响到外部代码的使用。同时,封装也为代码的重构和扩展提供了更大的灵活性。

  3. 简化代码调用过程:通过封装,可以将一些功能相似、但实现细节不同的方法进行统一的封装,从而简化了代码的调用过程,提高了代码的可读性和易用性。

此外,封装还通过定义私有变量和公有方法来实现,私有变量只能在类的内部访问,而公有方法可以在类的外部访问和调用。这种机制不仅加强了代码的安全性,也提高了代码的可维护性和重用性。封装的好处在于隐藏类的实现细节,让使用者只能通过程序员规定的方法来访问数据,可以方便地加入存取控制修饰符,来限制不合理操作。

Java封装的步骤

Java封装的步骤主要包括将类的属性声明为私有(private),并提供公共的setter()和getter()方法来访问和修改这些属性。这个过程不仅隐藏了类的内部实现细节,还通过方法对数据的访问和修改进行了控制,从而增强了程序的安全性和可维护性。

  1. 将属性私有化:这是封装的第一步,通过将类的属性声明为private,确保了这些属性只能被该类的方法直接访问,而不能被类外部的代码直接访问。这样做可以保护数据不被外部代码随意修改,从而提高了数据的安全性。

  2. 提供公共的setter和getter方法:为了允许外部代码访问或修改类的私有属性,需要提供公共的setter(用于设置属性值)和getter(用于获取属性值)方法。这些方法被称为“访问器”或“修饰符”,它们提供了对私有属性的公共访问接口。

  3. 构造方法的使用:在Java中,构造方法用于创建类的对象。在封装的过程中,构造方法也扮演着重要的角色,尤其是在需要初始化对象状态时。通过在构造方法中调用setter方法,可以实现对属性值的初始化,从而确保即使通过构造器初始化对象,也能实现数据验证的效果。

  4. 数据验证:在setter方法中,可以加入数据验证的逻辑,确保只有满足特定条件的数据才能被设置给属性。这种机制有助于保持数据的完整性和一致性,同时也提供了对数据输入的初步验证。

通过上述步骤,Java实现了面向对象的封装原则,即将数据和操作数据的代码捆绑在一起,形成一个独立的单元。这种机制不仅有助于保护数据的安全性和完整性,还使得代码更加模块化和可维护。

拦截

Java中实现AOP(面向切面编程)的一种常见方式是使用动态代理。以下是一个简单的例子,展示了如何使用动态代理来实现方法的拦截和增强。

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class AopExample { public static void main(String[] args) { TargetInterface target = new TargetImpl(); TargetInterface proxy = (TargetInterface) createProxy(target); proxy.businessMethod(); } public static Object createProxy(final TargetInterface target) { // 使用InvocationHandler来实现拦截逻辑 InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before method execution"); Object result = method.invoke(target, args); System.out.println("After method execution"); return result; } }; // 创建代理对象 return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); } // 目标对象接口 public interface TargetInterface { void businessMethod(); } // 目标对象实现 public static class TargetImpl implements TargetInterface { @Override public void businessMethod() { System.out.println("Business method executed"); } } }

在这个例子中,我们定义了一个TargetInterface接口和一个实现了该接口的TargetImpl类。createProxy方法负责创建代理对象,它使用了InvocationHandler来拦截所有的方法调用。当代理对象的方法被调用时,invoke方法会被执行,在方法执行前后打印出相应的消息。

运行main方法会输出:

Before method execution Business method executed After method execution

屏幕分辨率适配

本组做的一些项目其实不是很需要太复杂的屏幕适配,其实在这种环境下,让用户去适配设计稿明显是最优解。在用户打开网页的时候使用javascript对用户的屏幕和设计稿的尺寸做计算得出用户需要缩放的倍数可能会比编码适配要更好一些。在这种状态下,编码只需要用绝对布局按照设计稿的位置布局即可,

关于这方面,只需要复制如下的代码即可。

index.html

<!DOCTYPE html> <html lang="zh_CN"> <head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>放课后的下午茶时间</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body> <script> function resizeWindow() { // 设计稿:1920 * 1080 // 1.设计稿尺寸 let targetWidth = 1920; let targetHeight = 1080; let targetRatio = targetWidth / targetHeight; // 宽高比率 (宽 / 高) // 2.拿到当前设备(浏览器)的宽度和高度 let currentWidth = document.documentElement.clientWidth || document.body.clientWidth; let currentHeight = document.documentElement.clientHeight || document.body.clientHeight; // console.log(`${currentWidth}, ${currentHeight}`) // 3.计算缩放比率(屏幕过宽,根据高度计算缩放比例) // 若currentWidth是4k屏宽度 3840 除于 我们设计稿的宽度 1920 3840/1920 = 2 // 这样页面就行进行2倍缩放 let scaleRatio = currentWidth / targetWidth; // 参照宽度进行缩放(默认情况下) // 当前页面宽高比例,当页面越宽currentRatio值就越大 let currentRatio = currentWidth / currentHeight; // 判断是根据宽度进行缩放,还是根据高度进行缩放 if (currentRatio > targetRatio) { // 根据高度进行网页的缩放 scaleRatio = currentHeight / targetHeight; // 参照高度进行缩放(屏幕很宽的情况下) document.querySelector("#app").style = `transform: scale(${scaleRatio}) translateX(-50%)`; } else { // 根据宽度进行网页的缩放 document.querySelector("#app").style = `transform: scale(${scaleRatio}) translateX(-50%)`; } } resizeWindow(); window.addEventListener("resize", resizeWindow); </script> </html>

可能使用该代码之后还需要对标签的位置做出一定的调整,但说实话我并不记得适配的具体方式了,故将所有相关的css粘贴出来,供大家参考选用。

/* base.css */ *, *::before, *::after { box-sizing: border-box; margin: 0; font-weight: normal; } * { margin: 0; padding: 0; } body { overflow: hidden; width: 100vw; min-height: 100vh; } /* main.css */ @import './base.css'; #app { position: relative; width: 1920px; min-height: 1080px; transform-origin: left top; overflow: auto; left: 50%; } /* Main.vue */ .container { width: 100%; height: 1080px; display: flex; flex-direction: column; align-items: center; justify-content: center; } .title { position: absolute; left: 50%; top: 50%; transform-origin: center center; transform: translateX(-50%); opacity: 0; color: var(--color-text); font-size: 42px; }

测试

安全测试 密码安全性问题 在软件测试过程中,安全性问题是一个非常重要的方面,因为它关乎到系统和用户的信息安全。测试中使用的数据可能包含敏感信息,如个人身份信息、信用卡号码等。在测试过程中,必须确保这些敏感数据得到妥善处理,不会被泄露或滥用。在测试过程中,我们重点对登录功能安全性进行了处理。其中,测试人员需要验证系统在存储用户密码时是否采用安全的加密算法,如哈希函数,并是否使用适当的加盐机制。加盐可以防止彩虹表攻击,提高密码的安全性。而在确保在用户登录时,密码通过加密的传输层协议(如TLS或SSL)进行传输。这有助于防止中间人攻击,确保用户输入的密码在传输过程中不被窃取或篡改。我们还在系统中实施了强密码策略,包括密码长度、复杂性要求、定期密码更改等。测试应覆盖各种密码输入情境,确保系统正确地执行密码策略同时,由于系统支持多因素身份验证,测试其安全性。确保第二因素的传输和存储也是安全的,并测试在多因素身份验证场景下系统的整体安全性。在核查宝系统中,我们支持领导和普通管理员两种身份,因此测试这两种身份的整体安全性是十分重要的。最后,输入密码多次尝试失败后封禁账号是一种常见的安全策略,旨在防范恶意攻击者通过暴力破解密码的方式尝试入侵用户账户。这种策略有助于提高账户的安全性,但同时需要谨慎设计,以免影响正常用户的体验。 验证码安全性问题 验证码在安全性方面起到了重要的作用,主要用于验证用户身份、防止自动化攻击和确保用户与系统的交互安全。然而,在设计和使用验证码时,仍然可能面临一些安全性问题。恶意攻击者可能使用自动化工具,如爬虫或机器学习算法,来识别和破解验证码。测试应该验证验证码对抗自动化攻击的效果,确保其足够复杂和多样性,难以被自动程序破解。因此,我们选择后端生成验证码。后端生成验证码是一种用于验证用户身份、防范自动化攻击的常见安全策略。系统可以选择不同类型的验证码,如数字验证码、字母验证码、数字与字母组合的验证码、数学运算验证码等。验证码的类型应该根据具体的安全需求和用户体验来选择。验证码的长度通常是可配置的,根据安全要求可以选择适当的长度。一般来说,验证码长度越长,破解的难度就越大。系统可以设置验证码的复杂性,包括使用大小写字母、数字、特殊字符等。增加验证码的复杂性可以提高安全性,但也需要注意不要使验证码过于复杂而难以识别。为了防止验证码被攻击者截获后长时间使用,验证码通常具有一定的过期时间。过期时间可以根据具体需求进行配置,例如设置为几分钟。同时,我们也要确保每个验证码的唯一性,防止攻击者通过重复使用相同的验证码进行攻击。使用合适的算法和策略确保验证码的唯一性。如果验证码需要通过网络传输到客户端,确保在传输过程中采用安全的通信协议,如HTTPS,以防止中间人攻击。对于需要频繁刷新验证码的场景,我们还要考虑实现动态刷新机制,减少恶意攻击的可能性。 性能测试 性能测试是在软件开发过程中关键的一环,主要用于评估系统在不同负载条件下的性能、稳定性和可伸缩性。在进行性能测试时,可能会遇到一些常见的问题,下面是其中一些问题的描述: 定义合适的性能指标: 在性能测试之前,需要明确定义系统的性能指标,例如响应时间、吞吐量、并发用户数等。不清晰或不准确的性能指标可能导致对系统性能的错误评估。 负载模拟的真实性: 确保负载测试使用的负载模拟是真实和准确的。如果负载模拟不符合实际使用场景,测试结果可能不具有实际参考价值。 测试环境的还原性: 测试环境的还原性是指测试环境是否能够准确地还原生产环境的特性。如果测试环境与生产环境存在显著差异,测试结果可能不准确。 并发用户数设置: 确定适当的并发用户数是一个挑战。设置过低可能无法发现系统在高负载下的性能问题,而设置过高可能导致系统崩溃。需要根据系统的预期使用情况来调整并发用户数。 测试数据的真实性: 使用真实和合适的测试数据对性能测试至关重要。如果测试数据不真实或不具有代表性,测试结果可能无法反映系统在真实场景下的表现。 测试工具选择: 选择合适的性能测试工具是至关重要的。不同的工具适用于不同的场景,选择不当可能导致测试结果的不准确性。 测试时间的选择: 性能测试应该在软件开发的早期阶段开始,并且要在整个软件生命周期中定期进行。选择测试的时间点不当可能导致对系统性能问题的遗漏。 性能监控和分析: 确保在性能测试期间进行全面的监控和分析。及时识别和解决性能问题需要有效的监控工具和分析手段。 持续集成的性能测试: 将性能测试整合到持续集成和持续交付流程中,确保每次代码更改都能进行必要的性能验证。 容量规划: 根据性能测试结果进行容量规划,确保系统能够满足未来的用户和负载增长。 测试时遇见的问题 origin和refer匹配问题 在Web开发中,Origin 和 Referer(注意拼写是Referer而不是Referrer,尽管规范中使用的是Referrer,但由于历史原因,浏览器普遍使用Referer)是两个HTTP头字段,用于标识请求的来源。这两者经常用于实施跨站请求伪造(CSRF)和跨站脚本攻击(XSS)的防御机制。 Origin: Origin 是一个包含协议、域名和端口的字符串,用于表示请求的来源。例如,https://www.example.com 是一个Origin。浏览器在发送跨源请求时,会将请求的Origin信息包含在请求头中。服务器可以通过检查这个信息来确定是否允许跨源请求。 Referer: Referer 是一个包含完整URL的字符串,表示引导用户代理(通常是浏览器)发送当前请求的来源。例如,如果用户从https://www.example.com/page1点击链接跳转到https://www.example.com/page2,那么在发送到page2的请求头中会包含Referer: https://www.example.com/page1。 server暴露使用的技术resty和jetty Resty 不是一个独立的服务器,而是一个基于 OpenResty 平台的 Lua web 框架。OpenResty(也被称为 ngx_openresty)是一个集成了 Nginx 服务器和一系列常用的第三方模块的高性能 Web 平台。 OpenResty 提供了一种在 Nginx 服务器上通过 Lua 编程语言扩展和定制服务器行为的方法。Resty 则是一个基于这个平台的框架,它使得在 Nginx 中使用 Lua 编写的 web 应用变得更加简单和方便。 Resty 主要用于构建基于 Nginx 的高性能、低延迟的 Web 服务,特别适用于处理大量并发请求的场景。 Jetty: Jetty 是一个开源的、灵活的 Java HTTP 服务器和 Servlet 容器。它可以嵌入到 Java 应用程序中,也可以作为独立的服务器运行。 Jetty 提供了一种轻量级、灵活的方式来构建 Java Web 应用程序,支持 Servlet、WebSocket、HTTP/2 等标准。Jetty 的嵌入式特性使得将其嵌入到应用程序中变得相对简单,同时也支持作为独立的服务器运行。 Jetty 在 Java 生态系统中广泛应用,特别是在构建嵌入式应用、微服务、以及各种 Java Web 应用程序方面。

单机部署

由于项目要求,对于本项目来说,所有和业务相关的服务都是以最普通的方式部署的,其中包含了两个部分:

业务部分:Back, Neo4jBack, DatabaseBack, SparkBack, Front(OpenResty)

支撑部分:KingBase, Minio, Redis, OpenResty, Neo4j

其中支撑部分的部署我个人认为是比较成功的,业务部分的部署是存在一定的问题的。首先运行为纯后台运行,且服务下线后并没有完善的恢复流程,这其实是比较失败的设计。我个人认为最简单的也最好用的其实是用 SystemV 风格(init.d)或者 systemd (Service, systemctl)去管理服务,事实上本项目中支撑部分的部署就是上传二进制包然后写systemd脚本实现的,业务部分也理应如此,不过由于某些原因导致目前服务的部署还是原始的后台进程的方式,在新项目时不应该再出现类似的情况。

容器部署

本项目中与开发相关的服务部署在中容器中进行,包括代码仓库,maven及npm服务器,API文档平台等服务,在本项目刚接触到内网环境时,本人预期的容器化方案其实是systemd-nspawn(可以看作chroot的高级版),但很可惜在设计之初并没有考虑到服务器的支持情况,导致封装的所有服务都起不来,统一部署在一个环境下又会对环境造成一定的影响,故最终选择了使用Docker。在容器部署环节少不了镜像拉取环节,这个部分只能是尽量的满足,实在不行找替换。

文档模板集合

本部分集合了在交付阶段要求的某些文档的模板以及在开发过程中为了规范形成的某些文档。在需要相关的文档时可以取用

API文档模板

此段文字是本项目OpenAPI文档的首部介绍部分,可以针对项目做适当的修改。

XXXX项目API文档

1.概述

本文档只适用于前端和后端开发使用,API内容可能会发生改变,届时后端将会与前端就具体改动进行探讨,并将最终改动方案提交到本文档。

本文档中有关数据模型的相关陈述已在后端数据文档中详细论述,请参阅后端数据文档。

本文版本,修改日期,修改内容列表如下:

版本日期详情
v0.1xxxx年xx月xx日本版本为该项目的最初版本

2.基础规则

1.API接口

API统一使用RESTFul或类RESTful风格接口模式,(如果将来会有迭代的话,有可能)使用接口路径中的vX.X确认api版本。

应用的development环境入口为localhost:3000/

production环境入口待定。

2.API参数

API GET请求参数部分通过Query传输(如果有需要的话)

API POST请求参数部分使用content-type: application/json类型传输

API的所有返回值均为json格式,基础结构如下

{ "code": 0, //结果code "msg": "ok", //消息 "data": [] //返回信息 }

3.API状态码code

API状态码分为两种,成功并正常返回数据为0, 其他情况为五位且遵循如下格式:

  • 前两位代表出问题的微服务,目前编号如下:

    code状态
    00全局通用
    10网关
    11XXXXX后端
    12XXXXX服务
    13XXXXX服务
  • 中间一位代表出问题的类型,目前编号如下:

    code状态
    0正常情况,但需要处理(通常为队列等待中)
    1用户参数异常
    2本服务发生异常
    3其他服务发生异常
    4未预料到的异常
  • 最后两位代表出问题的具体信息,遵循如下规则:

    • 若为其他服务发生的异常,则为其他服务的编号
    • 若为未预料到的异常,则为00
    • 其他情况则为各微服务自定的编号

注意:当出现问题时,msg会标明错误具体消息

根据API执行成功与否,API请求的HTTP状态码会有如下类别:

code状态含义
200OK正常
400Bad Request接口发生错误
403Forbidden没有登录或没有权限
404Not Found没有找到接口
429Too many requests触发速率限制

数据文档模板

本项目的数据文档经历过两个版本,最初的版本是使用SQL建表语句表示的,在后期尝试过改为使用typescript表示,但这种表示方式不是很适合SQL数据库,更适合noSQL,故下面不赘述。

XXXX数据文档

一、概述

本文档是后端开发相关的数据文档,规定了表名,数据字段等,供数据库维护,后端开发以及参考使用。

二、具体字段

XXXX

简介,表权限,(如果来自ETL)来源,外键说明

数据字段如下:

TYPE_ VARCHAR (36 char) 类型 LITISTATION VARCHAR (36 char) LITISTATION XM VARCHAR (270 char) 姓名 SEX VARCHAR (36 char) SEX METHOD_ VARCHAR (222 char) METHOD SFZHM VARCHAR (60 char) 身份证号码 OTHERNO VARCHAR (90 char) OTHERNO ORGANIZATIONNO VARCHAR (108 char) ORGANIZATIONNO ADDRESS VARCHAR (294 char) ADDRESS TELEPHONE VARCHAR (112 char) TELEPHONE ID NUMERIC (18,0) ID

概要设计模板:

本模板来自Github Gist

XXX概要设计

概述

目的

本文档是针对XXX系统给出的系统概要设计文档,在本文档中,将给出XXX系统的系统设计原则、关键静态结构设计、关键动态流程设计、数据结构设计、人机交互设计、非功能性设计、系统部署与实施设计、版本维护设计等内容。

XXX系统的系统设计与实现基于XXX系统的需求分析,总体上将结合形式化设计的方法与文字描述,给出半形式化的概要设计与低层设计,与需求分析内容的相对应,以保证系统设计的严谨性与可实现性。在形式化部分,本文档将主要采取UML语言的包图、类图、序列图等进行系统设计。

本文档的适用读者为XXX系统的产品经理、设计人员、开发人员、测试人员以及后续维护人员。

设计原则

在本小节应阐述本系统在设计时主要考虑到的问题。例如文件搜索主要考虑的问题是搜索速度、资源占用、架构兼容等,窗口管理器主要考虑的问题是freedesktop的协议遵循性、已知的主要违反协议软件的绕过、渲染的性能等。

还可以在这里阐述在设计时主要考虑了哪些问题,例如为什么要使用某种开源协议、为什么要使用Qt而不是gtk,为什么要使用dtk,为什么要使用kprobes而不是ftrace,为什么要使用QtWidget而不是QML,为什么要使用QCef而不是QtWebengine或者QtWebkit等。

术语说明

如:

  • SSOOS:Security Subsystem Of Operating System,操作系统安全子系统

参考资料

如:

  • 沈晴霓,卿斯汉 操作系统安全设计 机械工业出版社 2013年9月

系统设计

在这里需要给出一个大图,或者文字说明本系统中所包含的所有子系统或者模块,以及本系统与哪些外部系统需要进行交互(编译和运行时都依赖于哪些软硬件,包括具体的版本)。 这里的图需要表现的是静态结构,用自己熟悉的工具做图, 图的内容可以是:

  • 整体系统结构
  • 分层图,也就是类似多层架构设计的图

关键结构的也可以使用表格来描述, 如:

| 成员名称 | 说明 | |----------+------| | XXX | XXXX |

模块结构

模块A

下面是每个主要模块(不需要写不重要的模块)的设计,包括模块内部的子模块结构、功能、子模块之间的接口,以及具体由哪个子模块实现父模块与外部模块之间的接口。

模块B

关键流程设计

关键流程指的是本系统中最关键的功能,例如对于文件搜索来说包括建立索引与文件搜索,对于窗口管理器来说是窗口的焦点、大小、位置与边框的管理。

在这里可以结合上述的静态模块结构设计与接口给出流程设计,例如模块A依赖于模块B,而且模块B提供了接口b1, 则在某个关键流程里应该就可以有A调用B的b1方法,传入某某参数,得到某某返回值的连线。 建议使用UML的序列图(如图2.5)、协作图(如图2.6)、活动图(如图2.7)、状态图(如图2.8)等来绘制,推荐使用序列图绘制, 状态图主要针对一个模块或者子模块内部的流程或者逻辑设计,其它的是多个模块之间的配合以完成功能的流程设计。

除了图以外,要在每个图下面说明

关键数据结构设计

包括应用软件外部存储的主要数据结构(例如文件搜索的索引文件内容),应用程序内部主要的数据结构、使用到的数据库的主要库表设计、关键的配置数据等。最好给出哪些数据结构被哪些模块使用到了。

主要人机交互设计

对于有图形界面的软件,应给出关键的人机交互设计,包括主要的界面线框图设计,以及各个界面之间的流转等。最好给出主要的图形界面与模块的对应关系。

非功能性设计

下面几项可以不用都写,因为不是每个软件都需要考虑到所有点。

性能

例如:

  • 启动时间设计优化
  • 资源(处理器、内存、硬盘、网络)占用设计优化
  • 减少对系统或者其他软件运行的性能影响设计考量
  • 数据吞吐量或者用户响应延迟设计优化

具体例如:

  • 使用了什么算法或者数据结构
  • 使用了什么高效的库
  • 使用了无锁设计或者高性能并发程序设计
  • 使用缓存
  • 使用了异步IO,等等。

可以结合前述的具体模块与流程来体现如何在模块设计时或者整体系统设计时考虑了性能问题。

安全性

例如:

  • 输入密码等认证信息是否使用了加密手段进行输入保护和数据传输
  • 加密强度是否高
  • 图形界面程序是否使用了root权限运行(含capabilities)运行

详细设计模板:

本模板来自Github Gist

XXX详细设计

概述

目的

本文档是针对XXX系统给出的系统详细设计文档,在本文档中,将给出XXX系统的系统模块列表,各模块的功能、输入、输出、逻辑流程与错误处理,以及各用户界面的详细流程等内容。

XXX系统的系统详细设计与实现基于XXX系统的概要设计,在其原则与基础上进行细化与划分,以为下一步的系统实现打下基础。

本文档的适用读者为XXX系统的产品经理、设计人员、开发人员、测试人员以及后续维护人员。

设计约束

例如应该遵循什么实现的原则,比如源码目录结构、简单的编码规则,每个源码文件应该包含什么版权申明,是否要做单元测试,是否采用CMake或者Makefile或者gcc/clang等构建工具等。

术语说明

如:

  • SSOOS:Security Subsystem Of Operating System,操作系统安全子系统

参考资料

如:

  • 沈晴霓,卿斯汉 操作系统安全设计 机械工业出版社 2013年9月

系统模块设计

在这里需要给出一个大图,或者文字说明本系统中所包含的所有子系统或者模块,以及本系统与哪些外部系统需要进行交互(编译和运行时都依赖于哪些软硬件,包括具体的版本)。 这里的图需要表现的是静态结构,用自己熟悉的工具做图, 图的内容可以是:

  • 整体系统结构
  • 分层图,也就是类似多层架构设计的图

关键结构的也可以使用表格来描述, 如:

| 成员名称 | 说明 | |----------+------| | XXX | XXXX |

模块 A 设计

模块 B 设计

全局数据结构设计

人机交互设计

关于内网开发

本项目中遇到了大量的需要进行内网开发的场景,这时候仅有一个代码编辑器是远远不够的,需要在内网部署一套开发环境。经过本人的测试,适合在内网环境下部署的开发环境有:

  • nexus 3:maven及npm私服(但是填充仓库稍有些麻烦)

  • gitlab ce:git仓库

  • portainer:Docker管理

  • yapi:API文档及测试平台(版本众多,无法推荐好用的版本)

    安装好上述环境即可以得到一个相对舒适的基本内网开发环境。当然,有一台可以联网的堡垒机其实是最好的选择,但是大多数情况下并没有相应的条件,所以只能适应环境了。

    其实根据软件项目开发的全流程来讲,还应该有一些其他的工具,比如项目管理,异常统计等还需要添加一些其他的服务,但在时间有限的情况下,部署好上述四个服务即可以开始开发了。

注:其实最好的内网开发方式是不进行内网开发,至少不要在一个毛皮房内进行内网开发,如果一个项目的内网环境没有任何的基础设施,需要你自己搭建的话,那我的建议还是不要把所有的开发过程放在内网环境中,设计好数据schema然后出一两个人对数据库中的数据进行分析,最后做ETL可能是更好的选择,但如果你非要进行内网开发的话,那我只能祝你好运,并希望你能正确的部署上述的基础设施了。

关于持续集成

在项目开发过程中,其实本人有考虑过利用Gitlab自带的runner功能对业务微服务进行CI/CD改造,但最后由于各方面的原因并没有真实落地。建议新项目在建立之初就考虑充分利用CI/CD服务,对所有人都方便。

其实也不需要太高深的CI/CD技术,只需要写一个Shell脚本跑通编译,部署,重启服务流程即可,但很可惜,在一个光秃秃的环境中我们实在是无法实现,希望下次在开发新项目的过程中可以好好的实践CI/CD。