2024年11月

前言

在实际工作中,我们需要经常跟第三方平台打交道,可能会对接第三方平台Controller接口,或者提供Controller接口给第三方平台调用。

那么问题来了,如果设计一个优雅的Controller接口,能够满足:安全性、可重复调用、稳定性、好定位问题等多方面需求?

今天跟大家一起聊聊设计Controller接口时,需要注意的一些地方,希望对你会有所帮助。

1. 签名

为了防止Controller接口中的数据被篡改,很多时候我们需要对Controller接口做
签名

接口请求方将
请求参数
+
时间戳
+
密钥
拼接成一个字符串,然后通过
md5
等hash算法,生成一个前面sign。

然后在请求参数或者请求头中,增加sign参数,传递给API接口。

API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。

如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。

如果两个sign不相等,则API接口的网关服务会直接返回签名错误。

问题来了:签名中为什么要加时间戳?

答:为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。

这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。

目前生成签名中的密钥有两种形式:

一种是双方约定一个固定值privateKey。

另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。

2. 加密

有些时候,我们的Controller接口直接传递的非常重要的数据,比如:用户的登录密码、银行卡号、转账金额、用户身份证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。

由此,我们需要对数据进行非对称加密。

目前使用比较多的是用
RSA

RSA包含了一对:
公钥

私钥

我们以用户登录密码为例。

在用户输入密码之后,在前端需要对密码使用公钥做加密处理。

公钥是保留在前端代码中的,即使泄露给别人了,也没关系。

因为使用公钥加密后的密码,只能使用后端服务中对应的私钥才能解密。

而我们私钥保存在后端服务的配置中,别人无法获取到。

因此,使用RSA加密和解密是安全的。

我们可以使用在线工具生成密钥对:https://tools.ytdevops.com/rsa-key-pair-generator

3. ip白名单

为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。

需求限制请求
ip
,增加
ip白名单

只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。

ip白名单也可以加在API网关服务上。

但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。

这时候就需要增加web防火墙了,比如:ModSecurity等。

4. 限流

如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。

第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。

由此,必须要对API接口做
限流

限流方法有三种:

  1. 对请求ip做限流:比如同一个ip,在一分钟内,对
    API接口总的请求次数
    ,不能超过10000次。
  2. 对请求接口做限流:比如同一个ip,在一分钟内,对
    指定的API接口
    ,请求次数不能超过2000次。
  3. 对请求用户做限流:比如同一个
    AK/SK用户
    ,在一分钟内,对API接口总的请求次数,不能超过10000次。

我们在实际工作中,可以通过
nginx

redis
或者
gateway
实现限流的功能。

5. 参数校验

我们需要对API接口做
参数校验
,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。

这样做可以拦截一些无效的请求。

比如在新增数据时,字段长度超过了数据字段的最大长度,数据库会直接报错。

但这种异常的请求,我们完全可以在API接口的前期进行识别,没有必要走到数据库保存数据那一步,浪费系统资源。

有些金额字段,本来是正数,但如果用户传入了负数,万一接口没做校验,可能会导致一些没必要的损失。

还有些状态字段,如果不做校验,用户如果传入了系统中不存在的枚举值,就会导致保存的数据异常。

由此可见,做参数校验是非常有必要的。

在Java中校验数据使用最多的是
hiberate

Validator
框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。

用它们校验数据非常方便。

当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。

6. 统一返回值

我之前调用过别人的API接口,正常返回数据是一种json格式,比如:

{
    "code":0,
    "message":null,
    "data":[{"id":123,"name":"abc"}]
},

签名错误返回的json格式:

{
    "code":1001,
    "message":"签名错误",
    "data":null
}

没有数据权限返回的json格式:

{
    "rt":10,
    "errorMgt":"没有权限",
    "result":null
}

这种是比较坑的做法,返回值中有多种不同格式的返回数据,这样会导致对接方很难理解。

出现这种情况,可能是API网关定义了一直返回值结构,业务系统定义了另外一种返回值结构。如果是网关异常,则返回网关定义的返回值结构,如果是业务系统异常,则返回业务系统的返回值结构。

但这样会导致API接口出现不同的异常时,返回不同的返回值结构,非常不利于接口的维护。

其实这个问题我们可以在设计
API网关
时解决。

业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。

所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。

7. 统一封装异常

我们的API接口需要对
异常
进行统一处理。

不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。

返回值中包含了
异常堆栈信息

数据库信息

错误代码和行数
等信息。

如果直接把这些内容暴露给第三方平台,是很危险的事情。

有些不法分子,利用接口返回值中的这些信息,有可能会进行sql注入或者直接脱库,而对我们系统造成一定的损失。

因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:

{
    "code":500,
    "message":"服务器内部错误",
    "data":null
}

返回码
code

500
,返回信息
message

服务器内部异常

这样第三方平台就知道是API接口出现了内部问题,但不知道具体原因,他们可以找我们排查问题。

我们可以在内部的日志文件中,把堆栈信息、数据库信息、错误代码行数等信息,打印出来。

我们可以在
gateway
中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。

8. 请求日志

在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。

我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。

最好有
traceId
,可以通过它串联整个请求的日志,过滤多余的日志。

当然有些时候,请求日志不光是你们公司开发人员需要查看,第三方平台的用户也需要能查看接口的请求日志。

这时就需要把日志落地到数据库,比如:
mongodb
或者
elastic search
,然后做一个UI页面,给第三方平台的用户开通查看权限。这样他们就能在外网查看请求日志了,他们自己也能定位一部分问题。

9. 幂等设计

第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做
幂等设计

也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。

这样做的目的是不会产生错误数据。

我们在日常工作中,可以通过在
数据库
中增加
唯一索引
,或者在
redis
保存
requestId
和请求参来保证接口幂等性。

对接口幂等性感兴趣的小伙伴,可以看看我的另一篇文章《
高并发下如何保证接口的幂等性?
》,里面有非常详细的介绍。

10. 限制记录条数

对于对我提供的批量接口,一定要
限制请求的记录条数

如果请求的数据太多,很容易造成API
接口超时
等问题,让API接口变得不稳定。

通常情况下,建议一次请求中的参数,最多支持传入500条记录。

如果用户传入多余500条记录,则接口直接给出提示。

建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。

11. 压测

上线前我们务必要对API接口做一下
压力测试
,知道各个接口的
qps
情况。

以便于我们能够更好的预估,需要部署多少服务器节点,对于API接口的稳定性至关重要。

之前虽说对API接口做了限流,但是实际上API接口是否能够达到限制的阀值,这是一个问号,如果不做压力测试,是有很大风险的。

比如:你API接口限流1秒只允许50次请求,但实际API接口只能处理30次请求,这样你的API接口也会处理不过来。

我们在工作中可以用
jmeter
或者
apache benc
对API接口做压力测试。

12. 异步处理

一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。

但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。

这种情况下,为了提升API接口的性能,我们可以改成
异步处理

在API接口中可以发送一条
mq消息
,然后直接返回成功。之后,有个专门的
mq消费者
去异步消费该消息,做业务逻辑处理。

直接异步处理的接口,第三方平台有两种方式获取到。

第一种方式是:我们
回调
第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。

第二种方式是:第三方平台通过
轮询
调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。

13. 数据脱敏

有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银行卡号等等。

这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。

这就需要对部分数据做
数据脱敏
了。

我们可以在返回的数据中,部分内容用
星号
代替。

已用户手机号为例:
182****887

这样即使数据被泄露了,也只泄露了一部分,不法分子拿到这份数据也没啥用。

14. 完整的接口文档

说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。

接口文档中需要包含如下信息:

  1. 接口地址
  2. 请求方式,比如:post或get
  3. 请求参数和字段介绍
  4. 返回值和字段介绍
  5. 返回码和错误信息
  6. 加密或签名示例
  7. 完整的请求demo
  8. 额外的说明,比如:开通ip白名单。

接口文档中最好能够统一接口和字段名称的命名风格,比如都用
驼峰标识
命名。

接口地址中可以加一个版本号v1,比如:v1/query/getCategory,这样以后接口有很大的变动,可以非常方便升级版本。

统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。

统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。

接口文档中写明AK/SK和域名,找某某单独提供等。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

本篇介绍Manim中创建三维立体的两个常用对象:
Cube

Prism

Cube
在制作动画时,可以用于展示立体几何中的立方体概念,或者通过旋转、缩放等动画效果来帮助理解三维空间中的几何变换。

Prism
是长方体,继承自
Cube
,与
Cube
相比,它可以更进一步设置不同的边长。

1. 主要参数

Cube
的主要参数有:

参数名称 类型 说明
side_length float 立方体的边长
fill_opacity float 立方体的透明度
fill_color Color 立方体的颜色
stroke_width float 设置立方体边框的宽度

Prism

Cube
的区别在于它没有
side_length
参数,取而代之是
dimensions
参数。

参数名称 类型 说明
dimensions tuple[float, float, float] 长方体的长,宽和高

Prism

dimensions
参数
长宽高
定义为同样的值时,就是
Cube

2. 主要方法

Cube

Prism
的没有特有的方法,对于通用的方法都支持,

比如
shift
(平移),
rotate
(旋转)和
scale
(缩放)等。

3. 使用示例

这个对象的使用比较简单,下面的示例演示其基本的使用和操作。

3.1. 默认显示

这个示例展示了如何在
Manim
中创建并显示一个立方体(
Cube
)和一个长方体(
Prism
)。

# 创建一个立方体
cube = Cube()
# 创建一个棱柱
prism = Prism()

3.2. 变换颜色

在这个示例中,首先创建了一个红色的立方体和一个蓝色的长方体。

接着,立方体逐渐变化为绿色,而长方体逐渐变化为黄色。

实际动画中,通过颜色的变换可以更好地演示形状属性的动态变化。

# 创建一个立方体
cube = Cube()
cube2 = Cube(fill_color=RED)

# 创建一个棱柱
prism = Prism()
prism2 = Prism(fill_color=GREEN)

3.3. 移动和旋转

此示例首先展示了一个立方体和一个长方体。

接下来,让立方体向右移动,而长方体向左移动,然后立方体顺时针旋转45度,而长方体则逆时针旋转45度。

这种移动和旋转的效果可以生动地展示三维空间中的几何变换。

# 创建一个立方体
cube = Cube(fill_color=RED)
# 创建一个棱柱
prism = Prism(fill_color=GREEN)

# 移动
self.play(
    cube.animate.shift(RIGHT),
    prism.animate.shift(LEFT),
)
# 旋转
self.play(
    cube.animate.rotate(PI / 4),
    prism.animate.rotate(-PI / 4),
)

3.4. 组合使用

在这个示例中,同样先创建一个立方体和一个长方体。

然后将这两个形状组合成一个整体,再整体向上移动,并旋转一个小角度。

这种组合使用的方式可以展示如何在
Manim
中处理和组织多个形状,以及如何对它们进行整体的动画效果。

# 创建一个立方体
cube = Cube(fill_color=RED)
# 创建一个棱柱
prism = Prism(fill_color=GREEN)
# 将立方体放在棱柱的右边
cube.next_to(prism, RIGHT)

# 组合使用
vg = VGroup(cube, prism)

# 整体移动组合
self.play(vg.animate.shift(UP))
# 整体旋转组合
self.play(vg.animate.rotate(PI / 2, axis=UP))

4. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
cube_prism.py
),

下载地址:
完整代码
(访问密码: 6872)

  1. @typescript-eslint/adjacent-overload-signatures
  • 建议函数重载的签名保持连续
  1. @typescript-eslint/await-thenable
  • 不允许对不是“Thenable”对象的值使用await关键字,相反对“Thenable”对象必须使用await,例如对Promise对象。
  1. @typescript-eslint/array-type
  • 定义数组时,使用统一的样式,如都使用T[]或都使用Array
"@typescript-eslint/array-type": [
  "error",
  {
    //      array | array-simple | generic
    "default": "array"
  }
]
  • default的值设置为array时,统一使用T[];设置generic时,统一使用Array
    ,设置为array-simple时,简单类型使用T[],其它类型使用Array
  1. @typescript-eslint/ban-ts-comment
  • 不允许使用
    @ts-<directional>
    格式的注释,或要求在注释后进行补充说明
  1. @typescript-eslint/ban-tslint-comment
  • 不允许使用
    //tslint:<rule-flag>
    格式的注释
  1. @typescript-eslint/ban-types
  • 不允许使用某些类型,例如类型小写保持一致,使用string,boolean,number等等,而不是String,Boolean,Number。
  1. @typescript-eslint/brace-style
  • 要求代码块的左大括号与其对应的语句或声明位于同一行。
  1. @typescript-eslint/class-literal-property-style
  • 建议类中的字面量属性对外暴露时,保持一致的风格
  1. @typescript-eslint/comma-dangle
  • 允许或禁止使用尾随逗号,类的最后一个属性或者数组最后一个元素禁止尾随逗号
"@typescript-eslint/comma-dangle": [
  "error",
  {
    //      never | always
    "arrays": "never",
    "objects": "never",
    "imports": "never",
    "exports": "never",
    "functions": "never"
  }
]
  • 共有数组arrays,对象objects,导入imports,导出exports和函数functions五各类型支持配置,值设置为never则是禁止尾随逗号,设置为always则是允许尾随逗号。
  1. @typescript-eslint/comma-spacing
  • 强制逗号前后的空格风格保持一致,例如强制要求逗号前不加空格,逗号后必须添加空格
"@typescript-eslint/comma-spacing": [
  "error",
  {
    "before": false,
    "after": true
  }
]
  1. @typescript-eslint/consistent-type-assertions
  • 强制使用一致的类型断言
  1. @typescript-eslint/default-param-last
  • 强制默认参数位于参数列表的最后一个
  1. @typescript-eslint/explicit-member-accessibility
  • 在类属性和方法上需要显式定义访问修饰符
  1. @typescript-eslint/func-call-spacing
  • 禁止或者要求函数名与函数名后面的括号之间加空格
"@typescript-eslint/func-call-spacing": [
  "error",
  "never"
]
  • 设置为never时,函数名后面禁止添加空格,设置为always时,函数名后面允许添加空格
  1. @typescript-eslint/init-declarations
  • 禁止或者要求在变量声明中进行初始化
"@typescript-eslint/init-declarations": [
  "error",
  "always"
]
  • 设置为always时,声明变量必须初始化,设置为never时,声明变量可以不初始化。
  1. @typescript-eslint/keyword-spacing
  • 强制在关键字之前和关键字之后保持一致的空格风格,例如在关键字前后都添加空格
"@typescript-eslint/keyword-spacing": [
  "error",
  {
    "before": true,
    "after": true
  }
]
  1. @typescript-eslint/lines-between-class-members
  • 禁止或者要求类成员之间有空行分隔,always为允许有空行,never为不允许有空行,如下设置空行后不加空行,属性和方法之前添加空行。
"@typescript-eslint/lines-between-class-members": [
  "error",
  {
    enforce: [
      {
        blankLine: "never",
        prev: "field",
        next: "method"
      }
    ]
  }
]
  1. @typescript-eslint/member-delimiter-style
  • 要求接口和类型别名中的成员之间使用特定的分隔符,支持定义的分隔符有三种:分号、逗号、无分隔符
  1. @typescript-eslint/member-ordering
  • 要求类、接口和类型字面量中成员的排序方式保持一致的风格
  1. @typescript-eslint/naming-convention
  • 强制标识符使用一致的命名风格。例如类名使用大驼峰,函数使用小驼峰。
  1. @typescript-eslint/no-array-constructor
  • 不允许使用“Array”构造函数。
  1. @typescript-eslint/no-base-to-string
  • 要求当一个对象在字符串化时提供了有用的信息,才能调用“toString()”方法
  1. @typescript-eslint/no-confusing-non-null-assertion
  • 不允许在可能产生混淆的位置使用非空断言
  1. @typescript-eslint/no-confusing-void-expression
  • 要求void类型的表达式出现在合适的位置
  1. @typescript-eslint/no-dupe-class-members
  • 不允许重复的类成员,即已经声明的成员属性,不允许重复再声明一次。
  1. @typescript-eslint/no-duplicate-imports
  • 禁止重复的模块导入,即已经导入的模块,不允许再再次导入。
  1. @typescript-eslint/no-empty-function
  • 不允许使用空函数,支持的白名单配置包括函数,箭头函数,方法,构造方法等等,配置如下
"@typescript-eslint/no-empty-function": [
  "error",
  {
    "allow": [
      "functions",
      "arrowFunctions",
      "generatorFunctions",
      "methods",
      "generatorMethods",
      "getters",
      "setters",
      "constructors",
      "asyncFunctions",
      "asyncMethods"
    ]
  }
]
  1. @typescript-eslint/no-empty-interface
  • 不允许声明空接口
  1. @typescript-eslint/no-extraneous-class
  • 不允许将类用作命名空间
  1. @typescript-eslint/no-extra-non-null-assertion
  • 不允许多余的非空断言
  1. @typescript-eslint/no-extra-parens
  • 禁止使用不必要的括号
  1. @typescript-eslint/no-extra-semi
  • 禁止使用不必要的分号
  1. @typescript-eslint/no-floating-promises
  • 要求正确处理Promise表达式,例如Promise一定要处理异常情况
  1. @typescript-eslint/no-implied-eval
  • 禁止使用类似“eval()”的方法
  1. @typescript-eslint/no-inferrable-types
  • 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明
  1. @typescript-eslint/no-invalid-this
  • 禁止在this的值为undefined的上下文中使用this
  1. @typescript-eslint/no-invalid-void-type
  • 禁止在返回类型或者泛型类型之外使用void
  1. @typescript-eslint/no-loss-of-precision
  • 禁止使用失去精度的字面数字
  1. @typescript-eslint/no-magic-numbers
  • 禁止使用魔法数字。但有些情况下我们又需要直接使用数字,例如定义枚举时,在数组中根据索引取数据时,或者直接定义某些值不是魔法数字,示例如下
"@typescript-eslint/no-magic-numbers": [
  "off",
  {
    "ignoreEnums": true,
    "ignoreArrayIndexes": true,
    "ignoreNumericLiteralTypes": true,
    "ignore": [
      -1,
      0,
      1
    ]
  }
]
  1. @typescript-eslint/no-misused-new
  • 要求正确地定义“new”和“constructor”
  1. @typescript-eslint/no-misused-promises
  • 禁止在不正确的位置使用Promise
  1. @typescript-eslint/no-non-null-asserted-optional-chain
  • 禁止在可选链表达式之后使用非空断言
  1. @typescript-eslint/no-non-null-assertion
  • 禁止以感叹号作为后缀的方式使用非空断言
  1. @typescript-eslint/no-redeclare
  • 禁止变量重复声明,即前面声明过的变量,不允许再次声明。
  1. @typescript-eslint/no-require-imports
  • 禁止使用“require()”语法导入依赖
  1. @typescript-eslint/no-restricted-syntax
  • 不允许使用指定的(即用户在规则中定义的)语法。例如不允许直接使用console.log打印日志,而是使用我们封装好的LogUtil打印日志
"@typescript-eslint/no-restricted-syntax": [
  "error",
  {
    "selector": "CallExpression[callee.name='console.log']",
    "message": "不要直接使用console打印日志,请使用LogUtil"
  }
]
  1. @typescript-eslint/no-shadow
  • 禁止声明与外部作用域变量同名的变量
  1. @typescript-eslint/no-throw-literal
  • 禁止将字面量作为异常抛出
  1. @typescript-eslint/no-unnecessary-boolean-literal-compare"
  • 禁止将布尔值和布尔字面量直接进行比较
  1. @typescript-eslint/no-unnecessary-condition
  • 不允许使用类型始终为真或始终为假的表达式作为判断条件
  1. @typescript-eslint/no-unnecessary-qualifier
  • 禁止不必要的命名空间限定符

书接上回,我们继续来分享一些关于特殊时间获取的常用扩展方法。

01
、获取当前日期所在月的第一个指定星期几

该方法和前面介绍的获取当前日期所在周的第一天(周一)核心思想是一样的,只是把求周一改成求周几而已,当然其中有些小细节需要注意,比如求所在周的第一天则两天都在同一周,而求所在月第一个指定周则可能两天在不同周,具体代码如下:

//获取当前日期所在月的第一个指定星期几
public static DateTime GetFirstDayOfWeekDateTimeInMonth(this DateTime dateTime, DayOfWeek dayOfWeek)
{
    //获取当前日期所在月的第一天
    var firstDayOfMonth = dateTime.GetFirstDayDateTimeOfMonth();
    //计算目标日期与当月第一天相差天数
    var diff = ((int)dayOfWeek - (int)firstDayOfMonth.DayOfWeek + 7) % 7;
    return firstDayOfMonth.AddDays(diff);
}

下面我们还需要做详细的单元测试,我们分别测试指定周一和周日两个特殊日期,然后再分别测试三种特殊情况:

指定周一测试:

(1) 验证当前日期是周五,而周一在下一周的情况;

(2) 验证当前日期是本月第一个周一的情况;

(3) 验证当前日期是周日,并且在本月第一个周一之后的情况;

指定周日测试:

(1) 验证当前日期是周五,并且在本月第一个周日之前的情况;

(2) 验证当前日期是本月第一个周日的情况;

(3) 验证当前日期是周一,并且在本月第一个周日之后的情况;

具体代码如下:

[Fact]
public void GetFirstDayOfWeekDateTimeInMonth()
{
    //验证当前日期是周五,而周一在下一周的情况
    var friday_monday = new DateTime(2024, 11, 1, 14, 10, 10);
    var day_friday_monday = friday_monday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 4), day_friday_monday);
    //验证当前日期是本月第一个周一的情况
    var monday_monday = new DateTime(2024, 11, 4, 4, 10, 10);
    var day_monday_monday = monday_monday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 4), day_monday_monday);
    //验证当前日期是周日,并且在本月第一个周一之后的情况
    var sunday_monday = new DateTime(2024, 11, 30, 4, 10, 10);
    var day_sunday_monday = sunday_monday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 4), day_sunday_monday);
    //验证当前日期是周五,并且在本月第一个周日之前的情况
    var friday_sunday = new DateTime(2024, 11, 1, 14, 10, 10);
    var day_friday_sunday = friday_sunday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Sunday);
    Assert.Equal(new DateTime(2024, 11, 3), day_friday_sunday);
    //验证当前日期是本月第一个周日的情况
    var sunday_sunday = new DateTime(2024, 11, 30, 4, 10, 10);
    var day_sunday_sunday = sunday_sunday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Sunday);
    Assert.Equal(new DateTime(2024, 11, 3), day_sunday_sunday);
    //验证当前日期是周一,并且在本月第一个周日之后的情况
    var monday_sunday = new DateTime(2024, 11, 4, 4, 10, 10);
    var day_monday_sunday = monday_sunday.GetFirstDayOfWeekDateTimeInMonth(DayOfWeek.Sunday);
    Assert.Equal(new DateTime(2024, 11, 3), day_monday_sunday);
}

02
、获取当前日期所在月的最后一个指定星期几

该方法和上一个求第一个指定星期几核心思想是一样的,具体代码如下:

//获取当前日期所在月的最后一个指定星期几
public static DateTime GetLastDayOfWeekDateTimeInMonth(this DateTime dateTime, DayOfWeek dayOfWeek)
{
    //获取当前日期所在月的最后一天
    var lastDayOfMonth = dateTime.GetLastDayDateTimeOfMonth();
    //计算目标日期与当月最后一天相差天数
    var diff = ((int)lastDayOfMonth.DayOfWeek - (int)dayOfWeek + 7) % 7;
    return lastDayOfMonth.AddDays(-diff);
}

单元测试可以参考求第一个指定星期几,这里就不赘述了。

03
、获取当前日期上一个指定星期几

求上一个指定周几,其实也不复杂,首先计算出当前日期与目标星期几相差的天数,其中有个小细节需要注意,就是如果两个日期相同,则需要把相差天数改为7,具体代码如下:

//获取当前日期上一个指定星期几
public static DateTime GetPreviousDayDateTimeOfWeek(this DateTime dateTime, DayOfWeek dayOfWeek)
{
    //计算当前日期与目标星期几相差天数
    var diff = ((int)dateTime.DayOfWeek - (int)dayOfWeek + 7) % 7;
    //如果相差0天表示当前日期和目标星期几相同,需要改为7
    diff = diff == 0 ? 7 : diff;
    return dateTime.AddDays(-diff).Date;
}

我们分别对以下四种情况做单元测试:

(1) 验证当前日期是周一,而上一个周一在上一月的情况;

(2) 验证当前日期是周一,而上一个周一在当月的情况;

(3) 验证当前日期是周日,而上一个周一在当月的情况;

(4) 验证当前日期是周六,并且是当月最后一天的情况;

具体代码如下:

[Fact]
public void GetPreviousDayDateTimeOfWeek()
{
    //验证当前日期是周一,而上一个周一在上一月的情况
    var monday = new DateTime(2024, 11, 1, 14, 10, 10);
    var day_monday = monday.GetPreviousDayDateTimeOfWeek(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 10, 28), day_monday);
    //验证当前日期是周一,而上一个周一在当月的情况
    var monday1 = new DateTime(2024, 11, 25, 14, 10, 10);
    var day_monday1 = monday1.GetPreviousDayDateTimeOfWeek(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 18), day_monday1);
    //验证当前日期是周日,而上一个周一在当月的情况
    var sunday = new DateTime(2024, 11, 24, 4, 10, 10);
    var day_sunday = sunday.GetPreviousDayDateTimeOfWeek(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 18), day_sunday);
    //验证当前日期是周六,并且是当月最后一天的情况
    var saturday = new DateTime(2024, 11, 30, 4, 10, 10);
    var day_saturday = saturday.GetPreviousDayDateTimeOfWeek(DayOfWeek.Monday);
    Assert.Equal(new DateTime(2024, 11, 25), day_saturday);
}

04
、获取当前日期下一个指定星期几

该方法和上面获取上一个指定星期几核心思想相同,具体代码如下:

//获取当前日期下一个最近指定星期几
public static DateTime GetNextDayDateTimeOfWeek(this DateTime dateTime, DayOfWeek dayOfWeek)
{
    //计算目标日期与当月最后一天相差天数
    var diff = ((int)dayOfWeek - (int)dateTime.DayOfWeek + 7) % 7;
    //如果相差0天表示当前日期和目标星期几相同,需要改为7
    diff = diff == 0 ? 7 : diff;
    return dateTime.AddDays(diff).Date;
}

单元测试也可以参考求上一个指定星期几,这里就不再赘述了。

05
、获取当前日期是其所在月的第几周

该方法的核心思想是,获取当前日期和当月第一天相差多少天,然后用相差的天数除以7即可获得当前是第几周。

但是这里有个比较麻烦的事情是如果第一周不满一周呢,比如当月的第一周第一天是2024-11-01周五,而今天是2024-11-07周四,应该是当月的第二周,但是如果直接计算两天的差再除以7结果显然是不对的。

因此我们首先需要把第一周不满一周的天数补上,即前面还有4天。

如此就是(7+4)/7=1…4,即所在第二周,其中商表示完整的周,余数则表示不完整的周。如果转为公式则为:days/7 + (days%7 > 0 ? 1 : 0),我们对这个公式简化后得到:(days + 6)/7,具体实现代码如下:

//获取当前日期是其所在月的第几周
public static int GetWeekOfMonth(this DateTime dateTime)
{
    //获取当前日期所在月的第一天
    var firstDayOfMonth = dateTime.GetFirstDayDateTimeOfMonth();
    //首先设定周一为一周的开始
    //计算当前月第一天与周一相差天数
    //即第一周如果不满一周还差多少天
    var diff = ((int)firstDayOfMonth.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7;
    //用第一周不满的差值加上当前日期的天数之和计算当前为当月第几周
    //然后计算 总天数/7的商,如果有余数则再加1
    //公式为:n/7 + (n%7 > 0 ? 1 : 0)
    //上面公式可以简化为 (n+6)/7
    return (diff + dateTime.Day + 6) / 7;
}

下面我们对其进行以下几种情况详细的单元测试:

(1) 验证当前日期是周五,且是当月第一天的情况;

(2) 验证当前日期是周日,且在当月第一周的情况;

(3) 验证当前日期是周一,且在当月第三周的情况;

(4) 验证当前日期是周日,且在当月第三周的情况;

(5) 验证当前日期是周六,且是当月最后一天的情况;

具体代码如下:

[Fact]
public void GetWeekOfMonth()
{
    //验证当前日期是周五,且是当月第一天的情况
    var friday = new DateTime(2024, 11, 1, 14, 10, 10);
    var day_friday = friday.GetWeekOfMonth();
    Assert.Equal(1, day_friday);
    //验证当前日期是周日,且在当月第一周的情况
    var sunday = new DateTime(2024, 11, 3, 14, 10, 10);
    var day_sunday = sunday.GetWeekOfMonth();
    Assert.Equal(1, day_sunday);
    //验证当前日期是周一,且在当月第三周的情况
    var monday = new DateTime(2024, 11, 11, 4, 10, 10);
    var day_monday = monday.GetWeekOfMonth();
    Assert.Equal(3, day_monday);
    //验证当前日期是周日,且在当月第三周的情况
    var date17 = new DateTime(2024, 11, 17, 4, 10, 10);
    var day17 = date17.GetWeekOfMonth();
    Assert.Equal(3, day17);
    //验证当前日期是周六,且是当月最后一天的情况
    var sunday1 = new DateTime(2024, 11, 30, 4, 10, 10);
    var day_sunday1 = sunday1.GetWeekOfMonth();
    Assert.Equal(5, day_sunday1);
}

06
、获取当前日期是其所在年的第几周(ISO 8601 标准)

在ISO 8601 标准规定中,每周从星期一开始,且每年最少有 52 周,每年的第一周是包含该年第一天的那一周,且该周必须至少有四天。

获取当然日期所在年的第几周可以通过调用C#中文化信息中日历组件中GetWeekOfYear方法,具体代码如下:

//获取当前日期是其所在年的第几周(ISO 8601 标准)
public static int GetWeekOfYear(this DateTime dateTime)
{
    var currentCulture = CultureInfo.CurrentCulture;
    return currentCulture.Calendar.GetWeekOfYear(dateTime, currentCulture.DateTimeFormat.CalendarWeekRule, currentCulture.DateTimeFormat.FirstDayOfWeek);
}

07
、获取当前日期所在月份的周数

该方法实现的核心思想是首先获取当前日期所在月份的第一天和最后一天,然后分别计算其所在当年第几周,最后相减即可得到,具体代码如下:

//获取当前日期所在月份的周数
public static int GetWeeksInMonth(this DateTime dateTime)
{
    //获取当前日期所在月的第一天
    var firstDayOfMonth = dateTime.GetFirstDayDateTimeOfMonth();
    //获取当前日期所在月的最后一天
    var lastDayOfMonth = dateTime.GetLastDayDateTimeOfMonth();
    //获取当月第一天在全年中的周数
    var firstWeek = firstDayOfMonth.GetWeekOfYear();
    //获取当月最后一天在全年中的周数
    var lastWeek = lastDayOfMonth.GetWeekOfYear();
    return lastWeek - firstWeek + 1;
}

08
、判断当前日期是否是周末

该方法比较简单,只是判断当前是否是否为周六或周日,具体代码如下:

//判断当前日期是否是周末
public static bool IsWeekend(this DateTime dateTime)
{
    return dateTime.DayOfWeek == DayOfWeek.Saturday || dateTime.DayOfWeek == DayOfWeek.Sunday;
}

09
、判断当前日期所在年是否是闰年

该方法调用了C#内置方法IsLeapYear,具体代码如下:

//判断当前日期所在年是否是闰年
public static bool IsLeapYear(this DateTime dateTime)
{
    return DateTime.IsLeapYear(dateTime.Year);
}

10
、获取当前日期所在季度

该方法也比较简单,只需要应用一个小公式即可求的,具体代码如下:

//获取当前日期所在季度
public static int GetQuarter(this DateTime dateTime)
{
    return (dateTime.Month - 1) / 3 + 1;
}

稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Ideal

转载请注明出处:

LVM(Logical Volume Manager,逻辑卷管理器)是一个用于Linux系统的磁盘管理工具。它提供了一种更加灵活的存储管理机制,可以方便地进行磁盘的扩容、缩减、快照以及迁移等操作。

基本概念

  1. 物理卷(PV):物理磁盘或分区,如
    /dev/sda1
  2. 卷组(VG):由一个或多个物理卷组成的集合。
  3. 逻辑卷(LV):从卷组中分配的逻辑磁盘,可以被文件系统格式化并用于存储数据。

安装LVM

在ubuntu系统可以通过下面的命令进行安装

# Ubuntu/Debian  
sudo apt
-get install lvm2

创建LVM

第一步:创建物理卷(PV)

假设有一个新的磁盘
/dev/sdb
,需要先将其初始化为物理卷:

sudo pvcreate /dev/sdb  

应用示例:

root@swan2:~# sudo pvcreate /dev/vdb
Physical volume
"/dev/vdb"successfully created.
root@sdwan2:
~# vgdisplay--- Volume group ---VG Name ubuntu-vg
System ID
Format lvm2
Metadata Areas
1Metadata Sequence No2VG Access read/write
VG Status resizable
MAX LV
0Cur LV1Open LV1Max PV0Cur PV1Act PV1VG Size<96.95GiB
PE Size
4.00MiB
Total PE
24818Alloc PE/ Size 12409 / 48.47GiB
Free PE
/ Size 12409 / 48.47GiB
VG UUID RCjkb6
-7ngM-9nss-OWOL-eqMR-9MDp-JCyLjk

第二步:创建卷组(VG)

创建一个名为
vg_data
的卷组,将新创建的物理卷加入其中:

sudo vgcreate vg_data /dev/sdb  

第三步:创建逻辑卷(LV)

创建一个名为
lv_data
的逻辑卷,大小为10G:

sudo lvcreate -n lv_data -L 10G vg_data  

第四步:格式化逻辑卷

对逻辑卷进行格式化,例如使用ext4文件系统:

sudo mkfs.ext4 /dev/vg_data/lv_data  

第五步:挂载逻辑卷

创建一个挂载点,然后将逻辑卷挂载到该挂载点:

mkdir /mnt/data  
sudo mount
/dev/vg_data/lv_data /mnt/data

扩容LVM

假设我们需要将逻辑卷
lv_data
扩展到20G,可以遵循以下步骤:

第一步:增加物理卷

假设在物理卷
/dev/sdb
上增加了空间(例如增加了第二个物理卷
/dev/sdc
),首先需要将新的物理卷初始化:

sudo pvcreate /dev/sdc  

然后,将其加入到卷组:

sudo vgextend vg_data /dev/sdc  

应用示例:

root@sdwan2:~# sudo vgextend ubuntu-vg /dev/vdb
Volume group
"ubuntu-vg"successfully extended
root@swan2:
~# vgdisplay--- Volume group ---VG Name ubuntu-vg
System ID
Format lvm2
Metadata Areas
2Metadata Sequence No3VG Access read/write
VG Status resizable
MAX LV
0Cur LV1Open LV1Max PV0Cur PV2Act PV2VG Size1.09TiB
PE Size
4.00MiB
Total PE
286961Alloc PE/ Size 12409 / 48.47GiB
Free PE
/ Size 274552 / <1.05TiB
VG UUID RCjkb6
-7ngM-9nss-OWOL-eqMR-9MDp-JCyLjk

第二步:扩展逻辑卷

使用以下命令将逻辑卷
lv_data
扩展到20G:

sudo lvextend -L 20G /dev/vg_data/lv_data  

或者,如果想使用所有可用的空间:

sudo lvextend -l +100%FREE /dev/vg_data/lv_data  

应用示例:

root@swan2:~# lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
Size of logical volume ubuntu
-vg/ubuntu-lv changed from 48.47 GiB (12409 extents) to 1.09 TiB (286961extents).
Logical volume ubuntu
-vg/ubuntu-lv successfully resized.
root@sdwan2:
~#

第三步:扩展文件系统

扩展完逻辑卷后,需要扩展文件系统以利用新增的空间:

sudo resize2fs /dev/vg_data/lv_data  

应用示例:

root@swan2:~# sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
resize2fs
1.45.5 (07-Jan-2020)
Filesystem at
/dev/ubuntu-vg/ubuntu-lv is mounted on /; on-line resizing required
old_desc_blocks
= 7, new_desc_blocks = 141The filesystem on/dev/ubuntu-vg/ubuntu-lv is now 293848064 (4k) blocks long.

root@swan2:
~#

查看LVM的信息

可以使用以下命令查看LVM的信息:

  • 查看所有物理卷:
    sudo pvdisplay  

  • 查看所有卷组:
    sudo vgdisplay  

  • 查看所有逻辑卷:
    sudo lvdisplay  

  • 查看详细的LVM状态:
    lvs